模板方法模式:定义、结构、实现、钩子方法、优缺点、应用场景

模式定义:说白了就是“骨架固定,细节可变”

模板方法模式,我个人的理解是:把一套流程的骨架定死,但允许子类去填充某些步骤的具体实现。你想想看,我们写代码时经常遇到这种情况——多个类做同一件事,步骤差不多,但中间某几步不一样。这时候要是每个类都写一遍完整流程,那代码冗余得让人抓狂。

我记得刚工作那会儿,接手一个支付系统。微信支付、支付宝、银联,流程都是“校验参数→调用接口→处理回调→更新订单”。但每个平台的签名算法、回调格式完全不同。我当时就用了模板方法模式,把公共流程写在父类里,子类只重写签名和解析回调的方法。嗯,代码量直接砍了一半。

核心思想:父类定义算法骨架,子类重写某些步骤,但不改变整体流程。

结构:就四个角色,别搞复杂了

模板方法模式的结构其实很简单,我画了一张图帮你理清关系:

AbstractClass(抽象父类) templateMethod() → 定义骨架 step1() / step2() / hook() ConcreteClassA step1() → 实现A step2() → 实现A ConcreteClassB step1() → 实现B step2() → 实现B 父类定义流程,子类实现细节

结构上就四个东西:

  • AbstractClass(抽象父类):定义模板方法和基本步骤。模板方法一般用 final 修饰,防止子类篡改流程。
  • ConcreteClass(具体子类):实现父类定义的抽象步骤,也可以覆盖钩子方法。
  • 模板方法:就是那个骨架方法,调用一系列步骤。
  • 钩子方法:这个我单独拿出来说,因为它太容易被忽略了。

实现:一个数据导出器的例子

光说理论没意思,我直接上个代码。假设我们要做一个数据导出工具,支持导出为 PDF 和 Excel。流程都是:查询数据 → 格式化 → 输出文件。但格式化的方式完全不同。

// 抽象父类
public abstract class DataExporter {
    
    // 模板方法,用 final 防止子类修改流程
    public final void export() {
        List<Data> data = fetchData();
        String formatted = formatData(data);
        saveToFile(formatted);
        if (needHeader()) {  // 钩子方法
            addHeader();
        }
    }
    
    // 公共步骤:查询数据
    private List<Data> fetchData() {
        System.out.println("从数据库查询数据...");
        return new ArrayList<>();
    }
    
    // 抽象步骤:格式化数据(子类必须实现)
    protected abstract String formatData(List<Data> data);
    
    // 公共步骤:保存文件
    private void saveToFile(String content) {
        System.out.println("保存文件到磁盘...");
    }
    
    // 钩子方法:默认不添加表头
    protected boolean needHeader() {
        return false;
    }
    
    // 钩子方法的具体操作
    private void addHeader() {
        System.out.println("添加文件表头...");
    }
}

// 具体子类:PDF导出
public class PdfExporter extends DataExporter {
    @Override
    protected String formatData(List<Data> data) {
        return "PDF格式的内容";
    }
    
    @Override
    protected boolean needHeader() {
        return true;  // PDF需要表头
    }
}

// 具体子类:Excel导出
public class ExcelExporter extends DataExporter {
    @Override
    protected String formatData(List<Data> data) {
        return "Excel格式的内容";
    }
    // 不重写 needHeader(),默认 false
}
我的习惯:模板方法尽量用 final 修饰。我曾经在一个项目里没加 final,结果新来的同事直接重写了模板方法,把整个流程改了,排查了半天才发现。加了 final,子类就只能乖乖实现步骤,流程不会乱。

钩子方法:一个容易被忽略的“开关”

钩子方法,说白了就是给子类一个“插一脚”的机会。父类定义了一个默认行为,子类可以选择覆盖它,也可以不覆盖。上面的 needHeader() 就是个典型的钩子。

我遇到过一种情况:某个导出功能,大部分时候不需要表头,但客户突然要求某个特定报表加表头。如果不用钩子,就得在子类里重写整个模板方法,那代码就乱了。用钩子方法,子类只需要重写一个返回 true 的方法,干净利落。

注意:钩子方法的名字要起得“像钩子”。比如 shouldDoSomething()needSomething(),一看就知道是条件判断。别起成 process() 这种模糊的名字,别人会以为这是必须实现的步骤。

优缺点:没有银弹,但很实用

优点 缺点
  • 代码复用性高,公共逻辑集中在父类
  • 扩展性好,新增子类只需实现特定步骤
  • 符合开闭原则,对扩展开放,对修改关闭
  • 子类数量增多时,类层次会变深
  • 父类对子类的控制是“白盒”的,子类需要了解父类的实现细节
  • 如果模板方法过于复杂,子类重写时容易出错

我个人觉得,模板方法模式最大的价值在于防止“复制粘贴式”的代码重复。你想想看,如果每个子类都写一遍完整的流程,一旦流程有变动(比如增加一个日志步骤),你得改多少个类?用模板方法,只改父类一处就够了。

应用场景:什么时候该用它?

我总结了几种典型场景,你在项目中可以对照着看:

  • 多个类有相同的算法骨架,但某些步骤不同——比如上面的数据导出例子。
  • 你想控制子类的扩展点——用钩子方法告诉子类“你只能在这里改,别的地方别动”。
  • 框架设计——很多框架都用模板方法定义核心流程,比如 Spring 的 JdbcTemplate,你只需要提供 SQL 和结果集处理逻辑,连接管理、事务控制都由模板方法搞定。
  • 单元测试中的“准备-执行-验证”模式——我写测试时经常用模板方法,把公共的 setup 和 teardown 放在父类,子类只写具体的测试逻辑。
避坑指南:我曾经在一个项目中过度使用模板方法,把整个业务逻辑都塞进了一个大模板方法里,结果子类重写了十几个步骤,代码反而更难维护了。记住:模板方法适合“流程稳定、步骤可变”的场景。如果流程本身也经常变,那不如用策略模式。

好了,模板方法模式就讲这么多。核心就一句话:把不变的流程锁死,把可变的部分开放。你在写代码时,如果发现好几个类都在重复同一个流程,只是中间某几步不一样,那模板方法模式就是你的最佳选择。


公众号:蓝海资料掘金营,微信deep3321