讲解

冲咖啡和泡茶的流程一模一样:烧水、冲泡、倒入杯中、加调料——变的只是「冲什么」和「加什么」。聪明的店家会把流程写成固定 SOP,店员只填两个空。模板方法模式就是这样:在一个方法里定义算法的骨架,把某些步骤延迟到子类中,让子类不改变算法结构即可重定义某些特定步骤。

这是 23 种模式里和「继承」绑定最深的一个:抽象类里那个固定的骨架方法(模板方法)依次调用若干步骤;步骤分三种——抽象方法(子类必须实现,比如 parse)、具体方法(父类已有默认实现)、钩子方法(hook,父类给空默认实现,子类可选覆盖,比如「是否加调料」)。模板方法本身最好声明为不可覆写(TypeScript 里靠约定 + readonly 语义,Java 里用 final),保证骨架不被子类篡改。

它体现的原则有个响亮的名字:好莱坞原则——「别打电话给我们,我们会打给你」。框架(父类)掌握控制权,在合适时机回调你的代码(子类);控制流是反转的。数据导入、骨架测试、CI 流水线(拉代码 → 装依赖 → 构建 → 部署)、游戏主循环,都是模板方法的天然形状。

和策略模式的分工:策略用组合换整个算法(整体替换);模板方法用继承改算法的某些步骤(骨架不变、环节可换)。流程的整体形状稳定、只有环节变化时用模板方法;整个算法都可以换时用策略。

示例

数据导入流水线:验证 → 解析 → 汇总的骨架固定,CSV 和 JSON 各自实现解析环节:

import assert from 'node:assert/strict';

interface RawRecord {
  name: string;
  score: number;
}

// 抽象类:run 是模板方法,骨架顺序不可变
abstract class DataImporter {
  run(raw: string): string {
    if (raw.trim().length === 0) {
      throw new Error('输入为空');
    }
    const records = this.parse(raw); // 抽象步骤:子类实现
    const passed = records.filter((r) => r.score >= 60); // 通用步骤:父类实现
    return '共 ' + records.length + ' 条,及格 ' + passed.length + ' 条';
  }

  protected abstract parse(raw: string): RawRecord[];
}

class CsvImporter extends DataImporter {
  protected parse(raw: string): RawRecord[] {
    return raw
      .trim()
      .split('\n')
      .map((line) => {
        const [name, score] = line.split(',');
        return { name, score: Number(score) };
      });
  }
}

class JsonImporter extends DataImporter {
  protected parse(raw: string): RawRecord[] {
    return JSON.parse(raw) as RawRecord[];
  }
}

const csv = new CsvImporter().run('小林,88\n小陈,55\n小周,72');
assert.equal(csv, '共 3 条,及格 2 条');

const json = new JsonImporter().run('[{"name":"小林","score":90}]');
assert.equal(json, '共 1 条,及格 1 条');

console.log('CSV 导入:' + csv);
console.log('JSON 导入:' + json);

注意控制权的方向:子类从来不调用 run 以外的流程,反而自己的 parse 是「被」父类的 run 调用的——好莱坞原则的直观体验。新增 XMLImporter 时只要实现 parse 一个方法,验证和汇总自动继承;而骨架本身(先验证后解析)在父类只有一份,改流程不用碰任何子类。

常见坑

  • 子类覆写模板方法本身:骨架被子类改掉,「固定流程」名存实亡;团队约定 + code review 守住,模板方法只被子类「使用」不被「改写」。
  • 抽象步骤过多:一个模板方法里挖了七八个抽象方法,子类实现到怀疑人生;步骤多时考虑拆成策略组合,或分层模板。
  • 钩子滥用成隐式开关:一堆空钩子让子类的行为依赖「覆写了哪几个」的隐式组合,可读性崩塌;钩子要有明确语义和文档。
  • 父类依赖子类的具体类型:父类里 instanceof 某个子类做分支,层次彻底颠倒;父类只能依赖抽象步骤。
  • 流程本身也在变还硬用模板方法:连骨架顺序都因场景而异的流程,该用建造者或流水线组合,模板方法的前提是骨架稳定。

小结

模板方法在父类固化算法骨架,子类填充可变步骤,控制权在父类(好莱坞原则);骨架稳定、环节可变是它的适用签名。下一章是 GoF 里公认的硬骨头:访问者模式。