模板回调模式:让模板方法更灵活

说实话,模板方法模式大家都不陌生。父类定义骨架,子类实现细节,很经典的套路。但我在实际项目中慢慢发现,继承的方式有时候太「重」了——为了一个回调方法,你得专门写个子类,用起来不够爽利。

模板回调模式,说白了就是「模板方法 + 回调接口」的组合拳。它用组合代替继承,用匿名实现类代替子类。嗯,Spring 的 JdbcTemplate 就是教科书级的案例。

从模板方法到模板回调

先看一个典型的模板方法例子:

// 模板方法风格
public abstract class DataProcessor {
    public final void process() {
        loadData();
        processData();
        saveData();
    }
    protected abstract void processData();
    // 其他方法...
}

// 使用:得写个子类
public class UserProcessor extends DataProcessor {
    @Override
    protected void processData() {
        // 业务逻辑
    }
}

你想想看,每次用都要 new 一个子类,或者写个匿名内部类。如果只是改一小段逻辑,这代价有点大。

模板回调模式怎么改?把抽象方法抽成接口:

// 回调接口
public interface DataCallback {
    void processData();
}

// 模板类
public class DataTemplate {
    public final void execute(DataCallback callback) {
        loadData();
        callback.processData();  // 回调
        saveData();
    }
    private void loadData() { /* ... */ }
    private void saveData() { /* ... */ }
}

// 使用:直接传 Lambda 或匿名类
DataTemplate template = new DataTemplate();
template.execute(() -> {
    // 业务逻辑,想写啥写啥
});

我个人习惯用这种方式。它把「变化的部分」通过参数传进去,而不是通过继承固定下来。灵活得多。

回调接口的设计要点

设计回调接口时,有几个坑我曾经踩过:

  • 接口粒度要适中——太粗了没法复用,太细了调用方写起来累
  • 考虑异常处理——回调里抛异常怎么办?模板要不要 catch?
  • 上下文传递——回调可能需要访问模板内部的临时数据
我曾经踩过的坑: 回调接口里塞了十几个方法,美其名曰「功能丰富」。结果调用方每次都得实现一堆空方法,代码又臭又长。后来我学乖了,一个接口最多两三个方法,多了就拆。

JdbcTemplate:模板回调的经典案例

Spring 的 JdbcTemplate 是模板回调模式的集大成者。它把 JDBC 的「获取连接→创建语句→执行→处理结果→关闭资源」这个固定流程封装起来,只把「处理结果集」这一块交给回调。

核心接口就两个:

接口 用途 典型场景
RowMapper<T> 将 ResultSet 的一行映射为对象 查询单条或多条记录
PreparedStatementCallback<T> 自定义 PreparedStatement 操作 批量更新、存储过程调用

看个实际用法:

// RowMapper 回调
List<User> users = jdbcTemplate.query(
    "SELECT * FROM users WHERE age > ?",
    new Object[]{18},
    (rs, rowNum) -> {
        User user = new User();
        user.setId(rs.getLong("id"));
        user.setName(rs.getString("name"));
        user.setAge(rs.getInt("age"));
        return user;
    }
);

// PreparedStatementCallback 回调
Integer count = jdbcTemplate.execute(
    "UPDATE users SET status = ? WHERE id = ?",
    (PreparedStatement ps) -> {
        ps.setInt(1, 1);
        ps.setLong(2, 100L);
        return ps.executeUpdate();
    }
);

你看,JdbcTemplate 把连接管理、异常转换、资源释放全包了。你只需要关心「怎么处理数据」这一件事。这就是模板回调的魅力。

核心逻辑流程图

下面这张图展示了模板回调模式的核心流程:

模板回调模式核心流程 调用方(Client) 模板类(Template) 回调接口(Callback) 调用 execute() 调用回调方法 1. 固定步骤(如打开连接) 2. 调用回调(变化点) 3. 固定步骤(如关闭资源) 回调实现 (Lambda / 匿名类) 模板类控制流程骨架,回调接口定义变化点,调用方提供具体实现

自己实现一个模板回调

光看不练假把式。咱们自己写一个简单的文件处理模板:

// 回调接口
@FunctionalInterface
public interface FileProcessorCallback {
    void process(BufferedReader reader) throws IOException;
}

// 模板类
public class FileTemplate {
    public void execute(String filePath, FileProcessorCallback callback) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(filePath));
            callback.process(reader);  // 回调
        } catch (IOException e) {
            // 统一异常处理
            throw new RuntimeException("文件处理失败: " + filePath, e);
        } finally {
            if (reader != null) {
                try { reader.close(); } catch (IOException ignored) {}
            }
        }
    }
}

// 使用
FileTemplate template = new FileTemplate();
template.execute("data.txt", reader -> {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
});
我的小建议: 回调接口尽量标注 @FunctionalInterface,这样调用方就能用 Lambda 表达式。代码会简洁很多。我在团队里推行这个做法后,新人上手快了不少。

模板回调 vs 模板方法:怎么选?

很多同学会问:什么时候用模板回调,什么时候用模板方法?我个人的经验是:

  • 用模板方法:当你有多个子类共享大部分逻辑,且子类之间有明显的「is-a」关系
  • 用模板回调:当变化点很零散,每次调用都可能不同,或者你不想为了一个方法就建个子类

说白了,模板回调更适合「一次性」或「轻量级」的场景。JdbcTemplate 就是典型——你不可能为每个 SQL 查询都写个子类吧?

避坑指南

我曾经犯过的错: 在回调接口里声明了受检异常(checked exception),结果调用方每次都得 try-catch,Lambda 表达式也用不了。后来我改成运行时异常,世界清净了。

还有一点要注意:回调里的资源管理。如果回调里打开了额外的资源(比如另一个文件流),记得在回调内部关闭。模板只负责它自己的资源,不负责你的。

嗯,模板回调模式就聊到这儿。它不是什么高深的概念,但用好了能让代码灵活不少。下次你写模板方法的时候,不妨想想:能不能用回调接口代替抽象方法?


公众号:蓝海资料掘金营,微信deep3321