mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3513 字
9 分钟
模板方法模式(Template Method Pattern)
2026-06-13

一、为什么需要模板方法模式#

你正在写一个数据处理框架,支持 CSV、JSON、XML 三种格式的导入。三种格式的处理流程完全一样:读取文件、解析数据、校验字段、写入数据库、发送通知。区别只在于「解析数据」这一步——CSV 按逗号分割,JSON 用反序列化,XML 用 DOM 解析。

最直接的做法是在每个导入器里把完整流程写一遍:

func ImportCSV(path string) error {
raw, err := readFile(path)
if err != nil { return err }
data, err := parseCSV(raw) // CSV 特有
if err != nil { return err }
if err := validate(data); err != nil { return err }
if err := writeToDB(data); err != nil { return err }
return notify("import done")
}
func ImportJSON(path string) error {
raw, err := readFile(path)
if err != nil { return err }
data, err := parseJSON(raw) // JSON 特有
if err != nil { return err }
if err := validate(data); err != nil { return err }
if err := writeToDB(data); err != nil { return err }
return notify("import done")
}

五个步骤,两个函数只有第二步不同,其余四步完全重复。如果以后要在「校验」和「写入」之间加一个「转换」步骤,你得改所有导入器。如果某个导入器忘了发通知,bug 就悄悄溜进去了。

模板方法模式解决的就是这个问题:把算法骨架放在基类里,把可变的步骤留给子类实现。基类定义一个 Import 方法,按固定顺序调用 ReadFileParseValidateWriteToDBNotify。其中 Parse 是抽象方法,子类各自实现;其余步骤有默认实现,子类也可以选择性覆写。

重构后,新增一种格式只需要实现 Parse 方法,骨架流程完全不用碰。

二、现实类比#

做菜。一道红烧肉的步骤是固定的:切块、焯水、炒糖色、加调料、炖煮、收汁。但「加调料」这一步因人而异——有人放八角桂皮,有人只放酱油冰糖。步骤骨架不变,具体某一步的内容可以替换。如果你把做菜写成代码,整个流程就是模板方法,「加调料」就是留给子类的钩子。

简历模板。不管谁写简历,结构都差不多:教育背景、工作经历、项目经验、技能特长。模板把章节顺序定死了,但每个章节填什么内容完全取决于个人。你不需要每次从空白文档开始排版,只需要在对应位置填上自己的信息。

流水线。汽车工厂的装配流程固定:底盘、发动机、车身、内饰、检测。不同车型走同一条流水线,区别只在具体零件——SUV 装大排量发动机,轿车装小排量。流水线的步骤顺序不会因为车型而改变,改变的只是每一步投入的物料。

这三个类比的共同点:流程骨架稳定,具体步骤可变。这正是模板方法模式的适用场景。

三、核心思想#

模板方法模式的核心结构很简单:一个抽象类定义模板方法(Template Method),模板方法按固定顺序调用若干原语操作(Primitive Operations)。其中部分原语操作是抽象的——子类必须实现;部分是钩子(Hook)——子类可以覆写,也可以用默认行为。

sequenceDiagram participant Client participant AbstractClass participant ConcreteClass Client->>AbstractClass: TemplateMethod() AbstractClass->>AbstractClass: step1() [固定步骤] AbstractClass->>ConcreteClass: step2() [抽象方法,子类实现] AbstractClass->>AbstractClass: step3() [钩子方法,可选覆写] AbstractClass->>ConcreteClass: step4() [抽象方法,子类实现] AbstractClass-->>Client: 返回结果

3.1 模板方法与原语操作#

模板方法本身是 final 或不可覆写的——它定义了算法的骨架,子类不能改变执行顺序。原语操作分两类:

  • 抽象方法:子类必须实现,没有默认行为。比如数据解析——每种格式必须自己实现
  • 钩子方法:子类可选覆写,有默认行为(通常是空实现)。比如发送通知——默认发邮件,子类可以改成发短信,也可以什么都不做

这种区分很实用。如果所有步骤都是抽象的,子类的工作量太大;如果所有步骤都有默认实现,又失去了扩展点。抽象方法保证关键步骤一定被实现,钩子方法提供灵活的定制空间。

3.2 好莱坞原则#

模板方法模式体现了好莱坞原则(Hollywood Principle):“Don’t call us, we’ll call you”(别找我们,我们会找你)。

在传统的调用关系中,子类主动调用基类的方法来复用逻辑。在模板方法中,关系反过来了——基类的模板方法调用子类实现的原语操作。子类不需要知道整个算法的流程,只需要实现被调用的那几个步骤。控制权在基类手里,子类被动响应。

这和框架的设计哲学一致:框架定义整体流程,用户代码填充具体逻辑。你写 Spring Controller 的时候不需要知道 DispatcherServlet 怎么分发请求,只需要实现 handleRequest。框架会来调用你,而不是你去调用框架。

3.3 Go 的实现方式#

Go 没有类继承,也没有 abstract 关键字。模板方法在 Go 里通过结构体嵌入实现:基结构体定义模板方法和默认实现,子结构体嵌入基结构体后覆写需要定制的方法。

关键技巧是:基结构体的模板方法通过调用自身方法来触发子结构体的覆写。Go 的方法解析规则是——如果嵌入结构体覆写了某个方法,调用时会优先使用覆写版本。这和 OOP 的虚方法分派效果一致。

3.4 JUnit 的生命周期钩子#

JUnit 的 @Before / @After 是模板方法在测试框架中的经典应用。测试框架的骨架是固定的:初始化环境、执行测试、清理环境。每个测试用例只需要写「执行测试」这一步,@Before@After 是可选的钩子——你可以覆写它们来定制初始化和清理逻辑,也可以不写,走默认的空实现。

四、变体与对比#

4.1 模板方法 vs 策略模式#

这两个模式经常被拿来比较,因为它们解决的是类似的问题——把算法的可变部分抽出来。但实现方式完全不同:

维度模板方法策略模式
机制继承——子类覆写步骤组合——替换整个策略对象
粒度算法骨架不变,替换个别步骤替换整个算法
绑定时机编译时(类定义时确定)运行时(可动态切换策略)
扩展性新增步骤需改基类新增策略只需加新类

模板方法通过继承控制算法骨架,子类只能替换步骤的实现,不能改变步骤的顺序和数量。策略模式通过组合替换整个算法,灵活性更高,但也意味着你需要自己保证不同策略的接口一致。

选择依据:如果算法骨架稳定、只是个别步骤不同,用模板方法;如果整个算法都可能替换,用策略模式。

4.2 钩子方法 vs 抽象方法#

钩子方法有默认实现,子类可选覆写;抽象方法没有默认实现,子类必须覆写。这个区分看似简单,实际设计时需要仔细权衡:

  • 把一个步骤设计成抽象方法,意味着所有子类都必须实现它——即使某些子类根本不需要这个步骤,也得写一个空实现
  • 把一个步骤设计成钩子方法,子类可能忘记覆写——因为编译器不会提醒你

实用建议:核心步骤用抽象方法,辅助步骤用钩子方法。「核心」指的是没有它算法就不完整——比如数据解析,不解析就没法继续。「辅助」指的是锦上添花——比如日志记录、通知发送,没有它们算法照样能跑。

4.3 函数式模板#

在支持高阶函数的语言里,模板方法可以用函数参数替代继承。模板方法变成一个接受函数参数的高阶函数,调用者传入具体的步骤实现:

// 函数式模板:把步骤作为参数传入
function processData(
raw: string,
parse: (raw: string) => Data[],
validate?: (data: Data[]) => void, // 可选钩子
notify?: (msg: string) => void, // 可选钩子
): void {
const data = parse(raw);
validate?.(data); // 有就调用,没有就跳过
writeToDB(data);
notify?.("import done");
}

函数式写法更轻量——不需要定义抽象类和继承关系,直接传函数就行。缺点是缺少了类继承带来的代码复用:如果多个调用者共享某些步骤的实现,用继承可以在基类里写一次,用函数参数就得各自传一遍。

五、多语言实现#

5.1 Go:数据处理管道#

用 Go 的结构体嵌入实现模板方法,模拟三种数据格式的导入管道:

package pipeline
import "fmt"
// DataImporter 数据导入管道——基结构体,定义模板方法
type DataImporter struct {
// 嵌入的子结构体会覆写 Parse 方法
parser func(raw string) ([]map[string]any, error)
}
// Import 模板方法——定义算法骨架,不可覆写
func (d *DataImporter) Import(path string) error {
// 步骤 1:读取文件(固定步骤)
raw, err := d.readFile(path)
if err != nil {
return fmt.Errorf("读取失败: %w", err)
}
// 步骤 2:解析数据(抽象步骤,子类必须提供)
data, err := d.parser(raw)
if err != nil {
return fmt.Errorf("解析失败: %w", err)
}
// 步骤 3:校验数据(钩子方法,有默认实现)
if err := d.Validate(data); err != nil {
return fmt.Errorf("校验失败: %w", err)
}
// 步骤 4:写入数据库(固定步骤)
if err := d.writeToDB(data); err != nil {
return fmt.Errorf("写入失败: %w", err)
}
// 步骤 5:发送通知(钩子方法,默认空实现)
d.Notify("import done: " + path)
return nil
}
// readFile 固定步骤——所有子类共享
func (d *DataImporter) readFile(path string) (string, error) {
// 实际实现省略,返回文件内容
return "file content", nil
}
// writeToDB 固定步骤——所有子类共享
func (d *DataImporter) writeToDB(data []map[string]any) error {
// 实际实现省略
return nil
}
// Validate 钩子方法——子类可选覆写,默认不做校验
func (d *DataImporter) Validate(data []map[string]any) error {
return nil // 默认:跳过校验
}
// Notify 钩子方法——子类可选覆写,默认不发通知
func (d *DataImporter) Notify(msg string) {
// 默认:静默,什么都不做
}
package pipeline
import "encoding/json"
// CSVImporter CSV 导入器——覆写解析步骤,定制校验钩子
type CSVImporter struct {
DataImporter // 嵌入基结构体,继承模板方法
}
func NewCSVImporter() *CSVImporter {
c := &CSVImporter{}
// 提供解析实现——相当于「抽象方法」的具体化
c.parser = c.parseCSV
return c
}
func (c *CSVImporter) parseCSV(raw string) ([]map[string]any, error) {
// CSV 解析逻辑
return []map[string]any{{"format": "csv"}}, nil
}
// Validate 覆写钩子方法——CSV 需要特殊校验
func (c *CSVImporter) Validate(data []map[string]any) error {
if len(data) == 0 {
return fmt.Errorf("CSV 数据为空")
}
return nil
}
// JSONImporter JSON 导入器——只覆写解析步骤
type JSONImporter struct {
DataImporter
}
func NewJSONImporter() *JSONImporter {
j := &JSONImporter{}
j.parser = j.parseJSON
return j
}
func (j *JSONImporter) parseJSON(raw string) ([]map[string]any, error) {
var result []map[string]any
if err := json.Unmarshal([]byte(raw), &result); err != nil {
return nil, err
}
return result, nil
}
// Notify 覆写钩子方法——JSON 导入需要发通知
func (j *JSONImporter) Notify(msg string) {
fmt.Println("[JSON Import]", msg)
}

Go 没有继承,所以用函数字段代替抽象方法——parser 字段在构造时赋值,效果等同于子类覆写。钩子方法则通过方法覆写实现:CSVImporter 覆写了 ValidateJSONImporter 覆写了 Notify,其余步骤走基结构体的默认实现。

这种写法的好处是:新增格式只需要新建一个结构体、提供 parser 函数、选择性覆写钩子方法。Import 方法的流程完全不用碰。

5.2 TypeScript:抽象类与函数式对比#

先用经典的抽象类方式实现,再用函数式写法对比:

// 抽象类方式——模板方法模式的标准实现
abstract class DataImporter {
// 模板方法——final,子类不能覆写
public import(path: string): void {
const raw = this.readFile(path);
const data = this.parse(raw); // 抽象方法,子类必须实现
this.validate(data); // 钩子方法,可选覆写
this.writeToDB(data);
this.notify('import done: ' + path); // 钩子方法,可选覆写
}
// 固定步骤
private readFile(path: string): string {
return 'file content'; // 实际实现省略
}
private writeToDB(data: Record<string, unknown>[]): void {
// 实际实现省略
}
// 抽象方法——子类必须实现
protected abstract parse(raw: string): Record<string, unknown>[];
// 钩子方法——有默认实现,子类可选覆写
protected validate(_data: Record<string, unknown>[]): void {
// 默认:跳过校验
}
protected notify(_msg: string): void {
// 默认:静默
}
}
// CSV 导入器
class CSVImporter extends DataImporter {
protected parse(raw: string): Record<string, unknown>[] {
// CSV 解析逻辑
return [{ format: 'csv' }];
}
// 覆写钩子——CSV 需要校验
protected validate(data: Record<string, unknown>[]): void {
if (data.length === 0) throw new Error('CSV 数据为空');
}
}
// JSON 导入器
class JSONImporter extends DataImporter {
protected parse(raw: string): Record<string, unknown>[] {
return JSON.parse(raw);
}
// 覆写钩子——JSON 需要通知
protected notify(msg: string): void {
console.log('[JSON Import]', msg);
}
}

TypeScript 的 abstract 关键字让模板方法的意图更清晰——parse 必须实现,validatenotify 可选覆写。编译器会检查子类是否实现了所有抽象方法,遗漏就报错。这比 Go 的函数字段更安全。

接下来看函数式写法:

// 函数式模板——用高阶函数替代继承
function createImporter(config: {
parse: (raw: string) => Record<string, unknown>[];
validate?: (data: Record<string, unknown>[]) => void;
notify?: (msg: string) => void;
}) {
return function importData(path: string): void {
const raw = readFile(path);
const data = config.parse(raw);
config.validate?.(data); // 可选钩子
writeToDB(data);
config.notify?.('import done: ' + path); // 可选钩子
};
}
// 使用——不需要定义类,直接传函数
const importCSV = createImporter({
parse: (raw) => [{ format: 'csv' }],
validate: (data) => {
if (data.length === 0) throw new Error('CSV 数据为空');
},
});
const importJSON = createImporter({
parse: (raw) => JSON.parse(raw),
notify: (msg) => console.log('[JSON Import]', msg),
});

函数式写法更简洁——没有类、没有继承、没有 abstractcreateImporter 返回一个闭包,闭包捕获了 config 中的步骤实现。可选步骤用 TypeScript 的可选属性 + 可选链 ?. 处理,效果等同于钩子方法的默认空实现。

两种写法的选择取决于场景:如果步骤之间有共享状态或需要代码复用,抽象类更合适;如果步骤都是无状态的纯函数,函数式写法更轻量。

六、生产验证#

  • JUnit 5 —— junit-team/junit5 测试生命周期是模板方法的经典应用。@BeforeAll / @BeforeEach / @Test / @AfterEach / @AfterAll 构成了测试执行的骨架。框架控制调用顺序,测试类只需要写 @Test 方法,@BeforeEach@AfterEach 是可选的钩子——你可以覆写它们来初始化和清理测试环境,也可以不写,走默认的空实现。JUnit 5 的 @BeforeAll 甚至支持用 default 方法在接口中提供默认初始化逻辑,这和模板方法中钩子方法的默认实现如出一辙。

  • Android Activity 生命周期 —— Android 源码 Activity 的生命周期回调 onCreateonStartonResumeonPauseonStoponDestroy 是模板方法的典型实现。Android 框架在 ActivityThread 中按固定顺序调用这些方法,开发者只需要覆写关心的回调。onCreate 是「抽象方法」——每个 Activity 必须实现它来初始化界面;onPauseonDestroy 是「钩子方法」——你可以覆写它们来保存状态和释放资源,也可以不写,走默认实现。整个生命周期的流转完全由框架控制,开发者无法改变调用顺序。

  • Spring Framework —— spring-projects/spring-framework AbstractControllerhandleRequest 方法是模板方法,内部按顺序调用 checkRequesthandleRequestInternalhandleInvalidRequesthandleRequestInternal 是抽象方法,子类必须实现;checkRequesthandleInvalidRequest 是钩子方法,有默认实现。Spring 中大量使用这种模式:AbstractViewrender 方法、AbstractApplicationContextrefresh 方法、AbstractHandlerMappinggetHandler 方法——框架定义流程,开发者填充逻辑。

七、小结#

何时使用

  • 框架生命周期——框架控制整体流程,用户代码填充具体步骤。JUnit 的测试生命周期、Android 的 Activity 生命周期、Spring 的 Controller 处理流程,都是模板方法在框架中的经典应用
  • 多个子类共享算法骨架——当多个实现只有个别步骤不同时,把骨架提到基类,避免重复代码
  • 需要控制扩展点——钩子方法让子类只能在你允许的地方定制,防止子类破坏算法的整体结构

何时不用

  • 算法整体需要替换——如果不同场景的算法骨架本身就不同,模板方法帮不了你,应该用策略模式
  • 步骤经常变化——如果算法的步骤顺序或数量频繁调整,每次改基类会影响所有子类,维护成本很高
  • 子类很少——如果只有一两个子类,抽象基类的开销不值得,直接写两个独立函数更简单

核心取舍:模板方法用继承换来了骨架的复用和扩展点的控制,代价是子类和基类的耦合。基类改了,所有子类都可能受影响。如果这种耦合可以接受——比如框架和用户代码的关系——模板方法就是最自然的选择。

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

模板方法模式(Template Method Pattern)
https://blog.souloss.com/posts/programming/behavioral/behavioral-template-method/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时