模板方法模式:算法骨架、钩子方法、实战:JdbcTemplate
说实话,模板方法模式是我在日常开发中用得最顺手的模式之一。它不像单例、工厂那样被天天挂在嘴边,但很多框架的核心流程都靠它来兜底。你想想看,如果每次写数据库操作都要重复“获取连接、创建语句、处理结果、关闭资源”这一套,那代码得多崩溃?
模板方法模式的核心思想很简单:把不变的流程封装在父类,把可变的部分留给子类去实现。说白了,就是“骨架我做主,细节你填充”。
算法骨架:不变的流程
我习惯把模板方法模式比作“做菜流程”。不管你是做红烧肉还是清蒸鱼,步骤都是:准备食材 → 烹饪 → 装盘。但具体怎么切、用什么调料、火候多大,那是子类的事。
来看一个简单的代码骨架:
public abstract class DataProcessor {
// 模板方法,定义算法骨架
public final void process() {
loadData();
validateData();
processData();
saveResult();
cleanup();
}
// 具体步骤,子类可重写
protected abstract void loadData();
protected abstract void processData();
// 可选步骤,子类可覆盖
protected void validateData() {
System.out.println("默认校验:数据非空");
}
// 固定步骤,子类不可修改
private void saveResult() {
System.out.println("保存结果到数据库");
}
// 钩子方法:子类可控制是否执行某一步
protected boolean needCleanup() {
return true;
}
private void cleanup() {
if (needCleanup()) {
System.out.println("清理资源");
}
}
}
注意看,process() 方法加了 final,这就是在告诉子类:“流程顺序你别动,动了就乱套了。” 我在项目中见过有人把模板方法去掉 final,结果子类重写后把校验步骤跳过了,线上出了数据问题。嗯,从那以后我写模板方法必加 final。
钩子方法:给子类留个“开关”
钩子方法(Hook)是模板方法模式里一个很巧妙的设计。它允许子类在特定节点“插一脚”,甚至决定某个步骤是否执行。
上面的 needCleanup() 就是一个钩子。默认返回 true,子类如果不需要清理资源,可以返回 false。比如某些只读查询,根本不需要清理写锁,就可以关掉这个步骤。
再举个例子,假设你做一个报表生成器,有的报表需要打印表头,有的不需要。你可以在父类里加一个 isShowHeader() 钩子,默认返回 true,子类按需覆盖。这样既保持了流程统一,又给了灵活性。
实战:JdbcTemplate 中的模板方法
说到实战,Spring 的 JdbcTemplate 是模板方法模式的经典案例。我刚开始用 Spring 时,只觉得它用起来方便,后来读了源码才恍然大悟——原来底层就是模板方法。
JdbcTemplate 的核心方法是 execute(),它定义了数据库操作的固定流程:
- 获取数据库连接
- 创建 PreparedStatement
- 设置参数(由回调接口实现)
- 执行 SQL
- 处理结果集(由回调接口实现)
- 清理资源(关闭连接、语句等)
其中,第3步和第5步是通过回调接口 PreparedStatementCallback 和 ResultSetExtractor 来让调用者自定义的。这其实就是模板方法模式的变种——用接口回调代替抽象方法,但思想完全一致。
// JdbcTemplate 简化版核心代码
public class JdbcTemplate {
public <T> T execute(ConnectionCallback<T> action) {
Connection con = null;
try {
con = getConnection(); // 固定步骤
return action.doInConnection(con); // 回调,由调用者实现
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
closeConnection(con); // 固定步骤
}
}
}
你看,getConnection() 和 closeConnection() 是固定的骨架,而具体的 SQL 操作通过回调交给调用者。这样既避免了重复的 try-catch-finally,又保留了灵活性。
模板方法 vs 策略模式:怎么选?
很多同学会混淆这两个模式。我简单总结一下:
| 维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 核心思想 | 固定算法骨架,子类重写部分步骤 | 将算法封装为独立策略,运行时切换 |
| 变化粒度 | 算法中的某一步或几步 | 整个算法 |
| 实现方式 | 继承 + 抽象方法 | 组合 + 接口 |
| 典型场景 | JdbcTemplate、Servlet 的 doGet/doPost | 支付方式选择、排序算法 |
说白了,如果你要控制流程的顺序,用模板方法;如果你只是要替换整个算法,用策略模式。两者也可以结合使用,比如模板方法内部调用策略接口,这在 Spring 源码里很常见。
SVG 流程图:模板方法模式的核心逻辑
下面这张图我画了好几次,力求把“骨架 + 钩子 + 子类实现”的关系讲清楚。你看一眼应该就能记住。
从图里可以清楚看到:父类定义了流程顺序,子类只负责实现抽象方法和覆盖钩子。流程的“方向盘”始终在父类手里。
避坑指南:我踩过的三个坑
- 坑一:模板方法里调用可重写的方法 —— 我曾经在父类构造器里调用了抽象方法,结果子类还没初始化完,导致空指针。记住:构造器里别调可重写的方法,这是 Java 的经典坑。
- 坑二:钩子方法太多 —— 有个项目我加了五六个钩子,结果子类代码里全是 if-else,反而比不用模板方法还乱。钩子一般控制在 1-2 个,多了就该考虑拆分类了。
- 坑三:忘记 final 模板方法 —— 前面提过,不加 final 就等于把流程控制权交给了子类。团队里如果有人不小心重写了模板方法,整个骨架就变形了。
好了,模板方法模式就聊到这儿。它不像那些花哨的模式,但胜在稳定、实用。下次你写重复的流程代码时,不妨想想:能不能把骨架抽出来?
公众号:蓝海资料掘金营,微信deep3321