什么是模板方法模式?应用场景有哪些?

#中间件#面试八股

什么是模板方法模式?应用场景有哪些?

结论先行:模板方法(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. 应用场景

框架/类模板方法子类要实现的步骤
JdbcTemplateexecute / query 的打开连接、执行、关连接RowMapper 做行映射
AbstractListadd/get 的默认实现get、size
HttpServletservice() 分派到 doGet/doPostdoGet、doPost
AQSacquire/release 的排队与唤醒tryAcquire、tryRelease
AbstractApplicationContextrefresh() 的 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()。
  • 追问重点:继承的僵化与"组合 + 回调"的替代方案。
笔记加载中…