模板方法模式:骨架与钩子的艺术
模板方法模式,说白了就是「把不变的流程固定下来,把可变的部分留给子类」。我刚开始接触设计模式时,觉得这玩意儿太简单了——不就是抽象类里写个方法,再让子类去实现吗?后来在项目中踩过坑才明白,真正用好它,关键在于理解「钩子方法」的灵活运用。
模式结构:骨架与步骤
模板方法模式的核心结构其实很清晰。它由两部分组成:
- 抽象模板类:定义算法的骨架,包含一个或多个模板方法
- 具体实现类:重写抽象方法,提供具体实现
我习惯把模板方法想象成「做菜的菜谱」。菜谱告诉你:先洗菜、再切菜、然后炒菜、最后装盘。这个流程是固定的。但具体洗什么菜、怎么切、用什么调料炒,这些细节可以自由发挥。模板方法模式就是这种思想在代码中的体现。
核心要点:模板方法模式定义了操作中的算法骨架,而将一些步骤延迟到子类中实现。它让子类在不改变算法结构的情况下,重新定义算法中的某些特定步骤。
来看一个典型的模板方法模式结构图:
模板方法的核心机制
模板方法模式中有几种关键的方法类型:
| 方法类型 | 说明 | 示例 |
|---|---|---|
| 模板方法 | 定义算法骨架,通常用 final 修饰 | templateMethod() |
| 抽象方法 | 子类必须实现的步骤 | step1(), step2() |
| 具体方法 | 所有子类共享的逻辑 | commonStep() |
| 钩子方法 | 可选扩展点,默认空实现 | hookMethod() |
我的经验:钩子方法是最容易被忽视但最有价值的部分。我曾经在一个支付系统中,用钩子方法实现了「是否发送通知」的控制。默认不发送,但某些子类可以重写钩子来开启通知。这样既保持了流程的统一,又给了子类灵活的选择权。
钩子方法:灵活性的开关
钩子方法,说白了就是「给你一个机会,在流程的某个节点插一脚」。它通常是一个空实现的方法,子类可以选择性地重写它。
为什么会需要钩子方法?你想想看,有时候子类并不想完全改变某个步骤,只是想在某些条件下做点额外的事情。比如:
- 在流程开始前检查某个条件
- 在流程结束后执行清理工作
- 控制流程中的某个分支是否执行
来看一个代码示例:
public abstract class DataProcessor {
// 模板方法
public final void process() {
loadData();
if (shouldValidate()) { // 钩子方法控制
validateData();
}
processData();
if (shouldLog()) { // 钩子方法控制
logResult();
}
cleanup();
}
protected abstract void loadData();
protected abstract void processData();
// 具体方法
private void validateData() {
System.out.println("验证数据...");
}
private void cleanup() {
System.out.println("清理资源...");
}
// 钩子方法 - 默认实现
protected boolean shouldValidate() {
return true; // 默认验证
}
protected boolean shouldLog() {
return false; // 默认不记录日志
}
protected void logResult() {
// 空实现,子类可重写
}
}
避坑指南:我曾经在一个项目中,把钩子方法设计成了抽象方法,结果每个子类都得实现它,哪怕大多数子类根本不需要。这完全违背了钩子方法的初衷。记住:钩子方法应该是可选的,不是强制的。
Spring JdbcTemplate 中的模板方法
说到模板方法模式的实际应用,Spring 的 JdbcTemplate 是个绝佳的例子。我刚开始用 Spring 时,只觉得它封装了 JDBC 的繁琐操作,后来看源码才发现,它骨子里就是模板方法模式。
JdbcTemplate 的核心流程是这样的:
- 获取数据库连接
- 创建 PreparedStatement
- 设置参数(由回调处理)
- 执行 SQL
- 处理结果集(由回调处理)
- 清理资源
这个流程是固定的,但「设置参数」和「处理结果」这两个步骤是变化的。Spring 用回调接口来处理这种变化:
// JdbcTemplate 的核心方法(简化版)
public <T> T execute(PreparedStatementCreator psc,
PreparedStatementCallback<T> action) {
// 1. 获取连接
Connection con = DataSourceUtils.getConnection(dataSource);
PreparedStatement ps = null;
try {
// 2. 创建 PreparedStatement
ps = psc.createPreparedStatement(con);
// 3. 执行并处理结果(回调)
return action.doInPreparedStatement(ps);
} catch (SQLException ex) {
// 异常处理
throw translateException(ex);
} finally {
// 4. 清理资源
JdbcUtils.closeStatement(ps);
DataSourceUtils.releaseConnection(con, dataSource);
}
}
你发现了吗?JdbcTemplate 的模板方法把「获取连接」和「清理资源」这些固定操作封装好了,而把「创建语句」和「处理结果」这些变化的部分通过回调暴露出去。这就是模板方法模式的精髓——把不变的和变的分开。
关键洞察:JdbcTemplate 用的是「回调式模板方法」,它没有用继承,而是用组合。这种方式更灵活,因为调用方不需要继承某个类,只需要实现一个接口。这是模板方法模式的一种变体,在 Spring 中非常常见。
模板方法 vs 策略模式
很多初学者会把模板方法和策略模式搞混。我简单说说它们的区别:
| 维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 关注点 | 算法的骨架和流程 | 算法的选择和替换 |
| 变化方式 | 子类重写部分步骤 | 替换整个策略对象 |
| 关系 | 继承关系(is-a) | 组合关系(has-a) |
| 控制权 | 父类控制流程(好莱坞原则) | 客户端选择策略 |
说白了,模板方法模式是「我给你一个框架,你在框架里填空」。策略模式是「我给你一个接口,你随便换实现」。两者各有适用场景,没有谁优谁劣。
我的建议:当你发现多个类有相似的流程,只是某些步骤不同时,优先考虑模板方法模式。当你发现某个行为有多种实现方式,且需要在运行时切换时,用策略模式。我在实际项目中,经常把两者结合使用——模板方法定义流程,策略模式处理流程中的某个复杂步骤。
实战中的注意事项
最后,分享几个我在实战中总结的经验:
- 模板方法不要太大:一个模板方法里不要塞太多步骤,否则子类实现起来会很痛苦。我一般控制在 5-7 个步骤以内。
- 钩子方法要有默认行为:要么是空实现,要么是合理的默认值。不要让子类必须重写钩子方法。
- 考虑用回调代替继承:如果子类只需要实现一两个方法,用回调接口可能比继承更灵活。JdbcTemplate 就是这么做的。
- 注意好莱坞原则:「不要打电话给我们,我们会打电话给你」。模板方法模式中,父类控制流程,子类不要反过来调用父类的方法。
曾经踩过的坑:我曾经在一个订单处理系统中,把模板方法设计得太「死」了。所有步骤都用 final 修饰,子类完全无法扩展。后来有个新需求要在订单处理中间加一个步骤,我不得不修改父类代码。这违反了开闭原则。从那以后,我学会了在模板方法中适当留一些「扩展点」——用钩子方法或者回调接口。
模板方法模式看起来简单,但用好它需要你对业务流程有深刻的理解。记住:它的核心不是「复用代码」,而是「复用流程」。当你把流程固定下来,变化的部分自然就清晰了。
公众号:蓝海资料掘金营,微信deep3321