设计模式-模板方法
模板方法模式
核心思想
在父类中定义算法的骨架(执行步骤和顺序),将某些步骤的具体实现延迟到子类。子类可以覆盖特定步骤,但不能改变整体流程结构。
这是一种基于继承的代码复用模式,解决的核心问题是:多个流程结构相同、但部分步骤实现不同的场景,如何消除重复代码。
结构
AbstractClass(抽象类)
├── templateMethod() // final,定义算法骨架,不允许子类覆盖
├── step1() // 具体方法,所有子类共用的逻辑
├── step2() // abstract,子类必须实现
└── hook() // 钩子方法,子类可选择性覆盖,默认空实现
ConcreteClassA extends AbstractClass
└── step2() // 实现自己的版本
ConcreteClassB extends AbstractClass
└── step2() // 实现自己的版本
代码示例
以数据导出场景为例:导出 Excel 和导出 CSV,流程相同(获取数据 → 转换格式 → 写入文件),只有转换和写入步骤不同。
抽象模板类:
public abstract class DataExporter {
/**
* 模板方法:定义导出流程,final 禁止子类覆盖整体流程
*/
public final void export(String query) {
List<Map<String, Object>> data = fetchData(query); // 公共步骤
String content = convert(data); // 抽象步骤,子类实现
beforeWrite(); // 钩子,子类可选覆盖
write(content); // 抽象步骤,子类实现
afterWrite(); // 钩子,子类可选覆盖
}
// 公共步骤:所有子类共用,直接在父类实现
private List<Map<String, Object>> fetchData(String query) {
System.out.println("执行查询: " + query);
return List.of(Map.of("id", 1, "name", "张三"));
}
// 抽象步骤:子类必须提供具体实现
protected abstract String convert(List<Map<String, Object>> data);
protected abstract void write(String content);
// 钩子方法:提供默认空实现,子类按需覆盖
protected void beforeWrite() {}
protected void afterWrite() {}
}
Excel 导出实现:
public class ExcelExporter extends DataExporter {
@Override
protected String convert(List<Map<String, Object>> data) {
// 转换为 Excel 格式(示意)
return "Excel内容: " + data;
}
@Override
protected void write(String content) {
System.out.println("写入 .xlsx 文件: " + content);
}
@Override
protected void afterWrite() {
System.out.println("发送导出完成邮件通知");
}
}
CSV 导出实现:
public class CsvExporter extends DataExporter {
@Override
protected String convert(List<Map<String, Object>> data) {
return "id,name\n1,张三";
}
@Override
protected void write(String content) {
System.out.println("写入 .csv 文件: " + content);
}
}
调用:
DataExporter exporter = new ExcelExporter();
exporter.export("SELECT * FROM users");
// 执行查询: SELECT * FROM users
// 写入 .xlsx 文件: Excel内容: [{id=1, name=张三}]
// 发送导出完成邮件通知
钩子方法的作用
钩子方法(Hook)是模板方法中的关键设计——父类提供默认实现(通常为空),子类可以选择性覆盖,用来在流程的特定节点插入逻辑,而不影响整体骨架。
钩子还可以用于控制流程是否执行某步骤:
public abstract class OrderProcessor {
public final void process(Order order) {
validate(order);
pay(order);
if (needsInvoice()) { // 通过钩子控制是否开票
generateInvoice(order);
}
notify(order);
}
protected abstract void pay(Order order);
// 钩子:默认不开票,子类可覆盖
protected boolean needsInvoice() {
return false;
}
private void validate(Order order) { /* 公共验证逻辑 */ }
private void notify(Order order) { /* 公共通知逻辑 */ }
private void generateInvoice(Order order) { /* 开票 */ }
}
// 企业订单:需要开票
public class EnterpriseOrderProcessor extends OrderProcessor {
@Override
protected void pay(Order order) { /* 对公转账 */ }
@Override
protected boolean needsInvoice() {
return true;
}
}
Spring 中的模板方法
Spring 框架大量使用模板方法模式,最典型的例子:
JdbcTemplate:
// Spring 内部 execute() 是模板方法,定义了:获取连接 → 创建Statement → 执行 → 释放连接
// 用户只需通过 callback 提供变化部分
jdbcTemplate.query("SELECT * FROM users", (rs, rowNum) -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
return user;
});
AbstractList:
// AbstractList 实现了 Iterator、indexOf、lastIndexOf 等通用方法
// 子类只需实现 get(int index) 和 size() 两个抽象方法
public class MyList extends AbstractList<String> {
private final String[] data;
public MyList(String[] data) { this.data = data; }
@Override
public String get(int index) { return data[index]; }
@Override
public int size() { return data.length; }
}
HttpServlet:
// service() 是模板方法,根据请求类型路由到 doGet/doPost
// 子类只需覆盖对应方法
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.getWriter().write("Hello");
}
}
与策略模式的对比
模板方法和策略模式都能解决"同一流程、不同实现"的问题,区别在于扩展方式。
| 对比项 | 模板方法模式 | 策略模式 |
|---|---|---|
| 实现机制 | 继承 | 组合 |
| 流程骨架 | 固定在父类,子类不可更改 | 无固定骨架,由 Context 控制 |
| 扩展方式 | 新增子类 | 新增策略实现类,注入 Context |
| 运行时切换 | 不支持 | 支持动态切换策略 |
| 适用场景 | 流程固定,步骤实现不同 | 算法可互换,需运行时选择 |
| 代码耦合 | 子类与父类紧耦合 | Context 与策略接口解耦 |
选择建议:
- 流程步骤顺序固定,只是部分步骤实现不同 → 模板方法
- 整个算法都可替换,或需要运行时动态切换 → 策略模式
- 实际项目中两者常配合使用:模板方法定骨架,策略模式填充可变步骤
适用场景
- 多个类有相同的执行流程,只有部分步骤实现不同(消除重复代码的首选)
- 框架设计中,框架定义流程骨架,使用者实现具体步骤(如 Spring 各种 Template)
- 需要控制子类扩展点,只允许子类修改特定步骤而不是整个流程
本博客所有文章除特别声明外,均采用
CC BY-NC-SA 4.0
许可协议,转载请注明出处!