什么是模板方法模式?应用场景有哪些?
结论先行:模板方法(Template Method)在父类里固定算法骨架,把其中会变的若干步骤声明为抽象方法或钩子(hook),交给子类填空。父类控制"什么时候做什么",子类只回答"这一步具体怎么做"——这就是好莱坞原则:别打给我们,我们会打给你。
1. 结构:骨架在父类,填空在子类
┌──────────────────────────────────────────────┐
│ AbstractImporter │
│ ──────────────────────────────────────────── │
│ + run(): ← 模板方法,final,不许改 │
│ 1. validate() ← 抽象,子类必须实现 │
│ 2. open() ← 抽象 │
│ 3. for each row: transform(row) + write(row) │
│ 4. afterImport() ← 钩子,默认空实现 │
│ 5. close() ← 抽象(或默认实现) │
└───────────────────┬──────────────────────────┘
│ 继承
┌───────────┴────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ CsvImporter │ │ ExcelImporter │
│ 实现 validate/ │ │ 实现 validate/ │
│ open/transform │ │ open/transform │
└──────────────────┘ └──────────────────┘
三个角色:模板方法(骨架,通常 final 防止被改)、抽象步骤(必须实现)、钩子方法(可选覆写,默认空)。
2. 代码:导入器的骨架
public abstract class AbstractImporter {
// 模板方法:流程固定,子类不能重写
public final int run(String path) {
validate(path);
open(path);
int ok = 0;
try {
for (Row row : readRows()) {
if (!filter(row)) { // 钩子:默认全通过
continue;
}
write(transform(row));
ok++;
}
afterImport(ok); // 钩子:默认空
} finally {
close(); // 无论成败都关闭资源
}
return ok;
}
protected abstract void validate(String path);
protected abstract void open(String path);
protected abstract Iterable<Row> readRows();
protected abstract String transform(Row row);
protected abstract void write(String record);
protected abstract void close();
// 钩子:默认实现,子类按需覆写
protected boolean filter(Row row) { return true; }
protected void afterImport(int count) { }
}
public class CsvImporter extends AbstractImporter {
private BufferedReader reader;
protected void validate(String path) {
if (!path.endsWith(".csv")) throw new IllegalArgumentException("只支持 csv");
}
protected void open(String path) {
try { reader = Files.newBufferedReader(Path.of(path)); }
catch (IOException e) { throw new UncheckedIOException(e); }
}
protected Iterable<Row> readRows() { return List.of(); } // 省略:解析每行为 Row
protected String transform(Row row) { return row.join(","); }
protected void write(String record) { System.out.println(record); }
protected void close() { /* 关闭 reader */ }
@Override
protected boolean filter(Row row) { return !row.isBlank(); } // 覆写钩子:跳过空行
}
骨架里 try/finally 保证资源释放,子类只关心"怎么读、怎么转",这是模板方法最实际的收益。
3. 与相邻模式的分工
| 对比项 | 模板方法 | 策略 | 工厂方法 |
|---|---|---|---|
| 复用手段 | 继承 | 组合 | 继承或反射 |
| 变化点 | 算法的某几步 | 整个算法 | 对象的创建 |
| 绑定时机 | 编译期(子类) | 运行期(注入) | 运行期(工厂决定) |
| 扩展方式 | 新增子类 | 新增实现类并注入 | 新增工厂/产品 |
| 缺点 | 继承耦合,层次深 | 类数量增加 | 类数量增加 |
模板方法和策略经常配合:骨架里留一个"策略步骤",把可替换的算法注入进来,兼顾固定流程与灵活替换。
4. 应用场景
| 框架/类 | 模板方法 | 子类要实现的步骤 |
|---|---|---|
JdbcTemplate | execute / query 的打开连接、执行、关连接 | RowMapper 做行映射 |
AbstractList | add/get 的默认实现 | get、size |
HttpServlet | service() 分派到 doGet/doPost | doGet、doPost |
AQS | acquire/release 的排队与唤醒 | tryAcquire、tryRelease |
AbstractApplicationContext | refresh() 的 12 步启动流程 | onRefresh、postProcessBeanFactory |
BaseExecutor(MyBatis) | 一级缓存 → 查库 → 二级缓存 | doQuery、doUpdate |
| 业务侧 | 导入/导出、对账、报表生成 | 各业务的具体读写 |
5. 常见追问
Q:为什么模板方法一般声明成 final?
骨架是契约。允许子类重写就等于允许绕过校验、跳过资源关闭,模板方法的意义就没了。需要扩展就开钩子,而不是重写骨架。
Q:继承会不会太死板?
会。类层次一深,改父类影响面很大(脆弱基类)。现代写法用组合 + 回调替代:把可变步骤作为函数参数传入,例如 jdbcTemplate.query(sql, rowMapper)——rowMapper 就是那个"填空"。所以模板方法的思想保留,实现手段从继承迁移到了函数式接口。
Q:钩子和抽象方法怎么选?
必须实现且没有合理默认值的用 abstract;可选、有默认行为的用钩子(空实现)。钩子的价值是"让子类在骨架的关键点上插手",比如 afterImport、onRefresh。
Q:模板方法和"控制反转"什么关系? 模板方法是 IoC 在类级别的体现:控制流由父类掌握,子类只被调用。Spring 容器则是框架级别的 IoC。
小结
- 模板方法 = 父类定骨架、子类填步骤,钩子提供可选插入点。
- 骨架声明
final,钩子默认空实现,资源释放放finally。 - 场景集中在框架基类:JdbcTemplate、HttpServlet、AQS、Spring
refresh()。 - 追问重点:继承的僵化与"组合 + 回调"的替代方案。