设计模式之模板方法模式
阅读指引:本文较长,可按需跳读。
- 想快速理解 → 读「概念」「举例理解」「工作流程」;
- 想看代码 → 读「代码示例」(含 UML、C++、Java、函数式轻量变体);
- 想区分相似模式 → 读「模板方法模式与策略模式的对比」;
- 想判断该不该用 → 读「优缺点」「一个反面例子」「应用场景」;
- 准备落地 → 读「注意事项」与「模板方法模式不负责什么」。
概念
定义
模板方法模式(Template Method Pattern) 是一种行为型设计模式。它的核心思想是:
在一个方法中定义一个算法的骨架,将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
换句话说:当一件事的流程骨架是固定的,但其中若干步骤的具体做法可能因场景而异时,可以把骨架写在父类的模板方法里,把可变步骤声明为抽象方法或钩子方法,交给子类实现。
结构
模板方法模式通常有两个角色(外加一个客户端):
- AbstractClass 抽象类
- 定义模板方法(
templateMethod()),它把算法骨架固定下来,通常声明为final,防止子类篡改流程。 - 声明抽象方法(primitive operations),由子类实现,是流程中的可变步骤。
- 提供具体方法,是流程中的固定步骤,子类可直接复用。
- 可选提供钩子方法(hook),给子类一个"可选覆盖"的扩展点。
- 定义模板方法(
- ConcreteClass 具体类
- 继承抽象类。
- 实现抽象方法,完成可变步骤。
- 按需覆盖钩子方法。
- Client 客户端
- 创建具体类实例,调用模板方法。客户端不关心流程细节,只触发一次调用。
这里要特别区分三类方法:
| 方法类型 | 谁实现 | 是否可覆盖 | 作用 |
|---|---|---|---|
| 模板方法 | 抽象类 | 通常 final,禁止覆盖 | 定义算法骨架与步骤顺序 |
| 抽象方法 | 子类 | 必须覆盖 | 流程中的可变步骤 |
| 具体方法 | 抽象类 | 可覆盖但不必须 | 流程中的固定步骤,可复用 |
| 钩子方法 | 抽象类 | 可选覆盖 | 扩展点,默认空实现或默认行为 |
举例理解
假设你要冲泡一杯饮品。不管是咖啡还是茶,流程骨架都差不多:
烧水 → 冲泡 → 倒入杯中 → 加调料
但"冲泡"和"加调料"这两步,咖啡和茶的具体做法不同:
- 咖啡:用咖啡粉冲泡,加糖和牛奶;
- 茶:用茶叶冲泡,加柠檬。
如果你用 if/else 写:
if (type == 咖啡) {
烧水();
用咖啡粉冲泡();
倒入杯中();
加糖和牛奶();
} else if (type == 茶) {
烧水();
用茶叶冲泡();
倒入杯中();
加柠檬();
}
烧水、倒杯这两步是重复的;新增一种饮品(比如可可)就要再加一个分支,流程骨架被分散在多个分支里,容易改漏。
模板方法模式的解决办法: 把流程骨架提到抽象类里,只把可变步骤延迟到子类。
-
抽象类:饮品
abstract class Beverage { // 模板方法:固定流程骨架,禁止子类覆盖 final void prepareRecipe() { boilWater(); // 固定步骤 brew(); // 可变步骤(抽象) pourInCup(); // 固定步骤 if (customerWantsCondiments()) { // 钩子 addCondiments(); // 可变步骤(抽象) } } abstract void brew(); abstract void addCondiments(); void boilWater() { ... } void pourInCup() { ... } // 钩子:默认需要加调料,子类可覆盖 boolean customerWantsCondiments() { return true; } } -
具体类:咖啡、茶
class Coffee extends Beverage { void brew() { 用咖啡粉冲泡(); } void addCondiments() { 加糖和牛奶(); } } class Tea extends Beverage { void brew() { 用茶叶冲泡(); } void addCondiments() { 加柠檬(); } // 覆盖钩子:茶默认不加调料 boolean customerWantsCondiments() { return false; } } -
客户端
Beverage coffee = new Coffee(); coffee.prepareRecipe(); // 烧水 → 咖啡粉冲泡 → 倒杯 → 加糖和牛奶 Beverage tea = new Tea(); tea.prepareRecipe(); // 烧水 → 茶叶冲泡 → 倒杯(钩子返回 false,跳过加调料)
模板方法就像"泡面包装上的步骤图":撕开调料包 → 加开水 → 盖盖等 3 分钟 → 拌匀。步骤顺序是固定的,但"加什么料""等多久"你可以自己定。包装不会因为你换了口味就改步骤图,流程骨架始终不变。
对比一下策略模式:策略模式是"我挑一个工具用",整个算法可替换;模板方法是"流程骨架我定好,你只填可变的那几步",算法结构不变,只换步骤实现。
工作流程
模板方法模式的工作流程可以理解成:
- 先定一个流程骨架:比如"冲泡饮品"的流程是烧水 → 冲泡 → 倒杯 → 加调料。
- 把固定步骤写进抽象类:烧水、倒杯是固定的,直接在抽象类里实现。
- 把可变步骤声明为抽象方法:冲泡、加调料因饮品而异,声明为
abstract,强制子类实现。 - 按需留钩子:比如"要不要加调料"这种可选行为,给一个默认实现,子类可覆盖。
- 模板方法锁定顺序:把上述步骤按顺序调用,声明为
final,子类不能改流程。 - 子类只填可变步骤:咖啡、茶各自实现
brew()和addCondiments(),流程骨架一行都不用改。 - 客户端调用模板方法:
coffee.prepareRecipe(),一次调用触发完整流程。
一句话概括:
父类定骨架 → 固定步骤父类写 → 可变步骤子类填 → 钩子留扩展 → 模板方法锁流程 → 客户端一次调用。
关键点:模板方法模式控制的是"流程骨架的复用",不是"算法的运行时替换"。 一个具体类实例调用模板方法时,走的就是它自己实现的那套步骤,不存在"执行到一半换另一个子类"的说法。
代码示例
UML
下面用"冲泡饮品"的例子,画出模板方法模式涉及的所有类,并标注关系。
| 类/接口 | 角色 | 在"冲泡饮品"里的含义 |
|---|---|---|
Beverage | 抽象类 AbstractClass | 定义冲泡饮品的流程骨架,固定步骤 + 抽象步骤 + 钩子 |
Coffee | 具体类 ConcreteClass | 实现咖啡的冲泡和加调料步骤 |
Tea | 具体类 ConcreteClass | 实现茶的冲泡和加调料步骤,并覆盖钩子决定不加调料 |
Client | 客户端 Client | 创建具体饮品并调用 prepareRecipe(),不关心流程细节 |
图中最重要的两条线:
- 具体类 → 抽象类:继承关系,子类复用流程骨架、只填可变步骤;
- Client → 抽象类:客户端面向
Beverage编程,调用模板方法,不依赖具体子类的流程细节。
与策略模式不同,模板方法模式里没有"上下文持有接口并委托"这一层,算法骨架就在抽象类里,子类通过继承参与流程。
C++ 实现
// ============================================================
// 模板方法模式(Template Method Pattern)完整示例
// 场景:冲泡饮品,流程骨架固定,冲泡与加调料因饮品而异。
//
// 对应 UML:
// Beverage —— 抽象类,定义模板方法与基本操作
// Coffee —— 具体类,实现咖啡的可变步骤
// Tea —— 具体类,实现茶的可变步骤并覆盖钩子
// Client —— 客户端,调用模板方法
// ============================================================
#include <iostream>
#include <memory>
#include <string>
// ------------------------------------------------------------
// 1. 抽象类 AbstractClass:饮品
//
// 说明:
// - prepareRecipe() 是模板方法,声明为 final(C++11 起可用),
// 防止子类改变流程骨架;
// - brew() / addCondiments() 是抽象方法,子类必须实现;
// - boilWater() / pourInCup() 是具体方法,固定步骤,子类可复用;
// - customerWantsCondiments() 是钩子方法,默认返回 true,
// 子类可按需覆盖。
// ------------------------------------------------------------
class Beverage {
public:
virtual ~Beverage() = default;
// 模板方法:固定流程骨架,禁止子类覆盖。
// 注意:模板方法本身通常不声明为 virtual,或声明为 final virtual。
void prepareRecipe() const {
boilWater();
brew();
pourInCup();
// 钩子:由子类决定是否需要加调料。
if (customerWantsCondiments()) {
addCondiments();
}
}
protected:
// 抽象方法:可变步骤,子类必须实现。
virtual void brew() const = 0;
virtual void addCondiments() const = 0;
// 具体方法:固定步骤,子类可直接复用。
void boilWater() const {
std::cout << " 烧水..." << std::endl;
}
void pourInCup() const {
std::cout << " 倒入杯中..." << std::endl;
}
// 钩子方法:默认需要加调料,子类可覆盖。
virtual bool customerWantsCondiments() const {
return true;
}
};
// ------------------------------------------------------------
// 2. 具体类 ConcreteClass:咖啡
// ------------------------------------------------------------
class Coffee : public Beverage {
protected:
void brew() const override {
std::cout << " 用咖啡粉冲泡咖啡..." << std::endl;
}
void addCondiments() const override {
std::cout << " 加糖和牛奶..." << std::endl;
}
// 不覆盖钩子,沿用默认 true(需要加调料)。
};
// ------------------------------------------------------------
// 3. 具体类 ConcreteClass:茶
// ------------------------------------------------------------
class Tea : public Beverage {
protected:
void brew() const override {
std::cout << " 用茶叶冲泡茶..." << std::endl;
}
void addCondiments() const override {
std::cout << " 加柠檬..." << std::endl;
}
// 覆盖钩子:茶默认不加调料。
bool customerWantsCondiments() const override {
return false;
}
};
// ------------------------------------------------------------
// 4. 客户端 Client
// ------------------------------------------------------------
int main() {
std::cout << "========== 冲泡咖啡 ==========" << std::endl;
std::unique_ptr<Beverage> coffee = std::make_unique<Coffee>();
coffee->prepareRecipe();
std::cout << "\n========== 冲泡茶 ==========" << std::endl;
std::unique_ptr<Beverage> tea = std::make_unique<Tea>();
tea->prepareRecipe();
// 说明:模板方法模式不支持"运行时切换流程实现"。
// 一个具体类实例调用模板方法时,走的就是它自己实现的那套步骤。
// 如果需要运行时替换整个算法,应使用策略模式。
return 0;
}
Java 实现
// ============================================================
// 模板方法模式(Template Method Pattern)完整示例(Java 版)
// ============================================================
// ------------------------------------------------------------
// 1. 抽象类 AbstractClass:饮品
// ------------------------------------------------------------
abstract class Beverage {
// 模板方法:固定流程骨架,声明为 final,禁止子类覆盖。
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
// 抽象方法:可变步骤,子类必须实现。
protected abstract void brew();
protected abstract void addCondiments();
// 具体方法:固定步骤,子类可直接复用。
protected void boilWater() {
System.out.println(" 烧水...");
}
protected void pourInCup() {
System.out.println(" 倒入杯中...");
}
// 钩子方法:默认需要加调料,子类可覆盖。
protected boolean customerWantsCondiments() {
return true;
}
}
// ------------------------------------------------------------
// 2. 具体类:咖啡
// ------------------------------------------------------------
class Coffee extends Beverage {
@Override
protected void brew() {
System.out.println(" 用咖啡粉冲泡咖啡...");
}
@Override
protected void addCondiments() {
System.out.println(" 加糖和牛奶...");
}
// 不覆盖钩子,沿用默认 true。
}
// ------------------------------------------------------------
// 3. 具体类:茶
// ------------------------------------------------------------
class Tea extends Beverage {
@Override
protected void brew() {
System.out.println(" 用茶叶冲泡茶...");
}
@Override
protected void addCondiments() {
System.out.println(" 加柠檬...");
}
// 覆盖钩子:茶默认不加调料。
@Override
protected boolean customerWantsCondiments() {
return false;
}
}
// ------------------------------------------------------------
// 4. 客户端 Client
// ------------------------------------------------------------
public class TemplateMethodDemo {
public static void main(String[] args) {
System.out.println("========== 冲泡咖啡 ==========");
Beverage coffee = new Coffee();
coffee.prepareRecipe();
System.out.println("\n========== 冲泡茶 ==========");
Beverage tea = new Tea();
tea.prepareRecipe();
// 设计取舍:
// 模板方法模式通过继承复用流程骨架,子类只填可变步骤。
// 代价是子类与父类强耦合,且无法在运行时切换"流程实现"。
// 如果业务需要运行时选择算法,应改用策略模式。
}
}
函数式轻量变体
模板方法模式的核心是继承 + 流程骨架复用,这跟函数式风格天然不太合拍。但如果不追求"继承"这一形式,只想要"固定流程 + 可变步骤",可以用高阶函数 + 组合来模拟,本质上更接近"函数式编排"。
import java.util.function.Consumer;
// 用函数式接口表示"可变步骤"
// 这里用 Consumer<String> 模拟"对饮品名做操作"的步骤
class FnBeverage {
private final Consumer<String> brew;
private final Consumer<String> addCondiments;
FnBeverage(Consumer<String> brew, Consumer<String> addCondiments) {
this.brew = brew;
this.addCondiments = addCondiments;
}
// 流程骨架:固定顺序,可变步骤以函数形式注入
void prepareRecipe(String name) {
boilWater();
brew.accept(name);
pourInCup();
addCondiments.accept(name);
}
private void boilWater() {
System.out.println(" 烧水...");
}
private void pourInCup() {
System.out.println(" 倒入杯中...");
}
}
public class FnTemplateDemo {
public static void main(String[] args) {
FnBeverage coffee = new FnBeverage(
name -> System.out.println(" 用咖啡粉冲泡" + name + "..."),
name -> System.out.println(" 加糖和牛奶...")
);
FnBeverage tea = new FnBeverage(
name -> System.out.println(" 用茶叶冲泡" + name + "..."),
name -> System.out.println(" 加柠檬...")
);
System.out.println("========== 冲泡咖啡 ==========");
coffee.prepareRecipe("咖啡");
System.out.println("\n========== 冲泡茶 ==========");
tea.prepareRecipe("茶");
}
}
取舍对照:
| 维度 | 传统模板方法(继承) | 函数式编排(组合) |
|---|---|---|
| 复用机制 | 继承,子类复用父类骨架 | 组合,高阶函数注入步骤 |
| 流程骨架位置 | 抽象类的 final 模板方法 | 普通方法或独立流程函数 |
| 扩展方式 | 新增子类 | 传入不同的函数 |
| 是否强制流程固定 | 是(final 锁死) | 取决于实现,可松可紧 |
| 钩子支持 | 天然支持(虚方法) | 需额外参数或 Optional 模拟 |
| 类爆炸风险 | 每个变体一个子类 | 无新增类,只有函数 |
| 可读性 | 类名即文档,流程一目了然 | 短逻辑清晰,长逻辑易失控 |
经验小谈:如果流程骨架固定、可变步骤有 2~4 个、且需要被继承体系管理(比如框架扩展点),用传统模板方法;如果只是"固定顺序 + 几个可变动作"、不需要继承、逻辑简单,函数式编排更轻。但要注意:函数式编排已经偏离了模板方法模式的本意,更接近"策略 + 流程组合",不要为了套模式而硬改。
模板方法模式与策略模式的对比
模板方法模式和策略模式经常被放在一起比较,因为两者都涉及"算法的可变性"。但它们的实现机制、控制方向、适用场景完全不同。
一、主要区别
- 模板方法模式:用继承复用流程骨架,父类控制流程,子类只填可变步骤。算法的结构不变,步骤实现可变。
- 策略模式:用组合持有可替换的算法对象,客户端在运行时选择整个算法。算法的整体可替换。
用"冲泡饮品"类比:
- 模板方法:冲泡流程(烧水 → 冲泡 → 倒杯 → 加调料)是固定的,你只能改"冲泡"和"加调料"两步;
- 策略模式:整个"冲泡饮品"的算法就是一个策略,你可以换成"冲泡咖啡策略"“冲泡茶策略”“冲泡可可策略”,连流程都可以不同。
二、核心差异
| 对比维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 核心机制 | 继承 | 组合 |
| 可变范围 | 流程中的某些步骤 | 整个算法 |
| 流程骨架 | 固定在父类模板方法中 | 不在模式内约束,由策略自行决定 |
| 扩展方式 | 新增子类,覆盖抽象/钩子方法 | 新增策略类,实现策略接口 |
| 运行时切换 | 不支持(实例化哪个子类就用哪套) | 支持(setStrategy() 替换引用) |
| 耦合度 | 子类与父类强耦合 | 上下文只依赖策略接口,耦合较低 |
| 控制方向 | 父类调用子类(好莱坞原则) | 上下文委托策略(组合) |
| 开闭原则 | 骨架不变,步骤可扩展 | 算法整体可扩展 |
| 典型变化 | 新增一种具体流程变体 | 新增一种算法 |
| 客户端职责 | 选择具体子类,调用模板方法 | 选择策略、注入、可运行时切换 |
最关键的一条:
模板方法是"流程我定,你只填几步";策略模式是"整个算法你随便换,我只负责调用"。
三、用"冲泡饮品"分别实现,对比更直观
模板方法版本(继承,流程固定):
Beverage coffee = new Coffee();
coffee.prepareRecipe(); // 烧水 → 咖啡粉冲泡 → 倒杯 → 加糖和牛奶
Beverage tea = new Tea();
tea.prepareRecipe(); // 烧水 → 茶叶冲泡 → 倒杯(钩子跳过加调料)
- 流程骨架在
Beverage.prepareRecipe()里,final锁死; - 子类只能改
brew()、addCondiments()和钩子; - 不能在运行时把
coffee变成tea的流程。
策略模式版本(组合,整体可换):
BeverageMaker maker = new BeverageMaker(new CoffeeStrategy());
maker.prepare(); // 按咖啡策略冲泡
maker.setStrategy(new TeaStrategy()); // 运行时切换
maker.prepare(); // 按茶策略冲泡
- 整个冲泡算法被封装成策略,流程由策略自己决定;
- 上下文只持有策略接口,可在运行时替换;
- 策略之间互不知道彼此。
四、判断用哪个
| 问题 | 选模板方法 | 选策略 |
|---|---|---|
| 流程骨架是否固定? | 固定,只有少数步骤可变 | 不固定,整个算法可替换 |
| 是否需要运行时切换? | 不需要 | 需要 |
| 可变部分占整体比例? | 小(局部步骤) | 大(整体算法) |
| 是否愿意用继承? | 可以,且希望复用骨架 | 不希望继承,倾向组合 |
| 变化的核心是什么? | 同一流程的不同变体 | 同一行为的不同算法 |
五、常见误区
- 误区 1:模板方法就是"带 final 方法的策略模式"。 不是。模板方法靠继承复用骨架,策略靠组合替换整体;前者流程固定,后者算法整体可换。
- 误区 2:模板方法不能有钩子。 钩子正是模板方法的扩展点,合理使用钩子能让子类按需介入流程,而不必重写整个骨架。
- 误区 3:模板方法和策略模式不能共存。 可以。比如支付流程(模板方法固定"校验→扣款→记录→通知"),其中"扣款"这一步用策略模式支持微信/支付宝/银行卡。
- 误区 4:模板方法一定比策略模式"低级"。 两者没有高下之分,只是适用场景不同。流程固定时,模板方法比策略模式更直接、更省类。
六、典型使用场景对照
| 模板方法模式 | 策略模式 |
|---|---|
Servlet 的 service() → doGet()/doPost() | 支付方式:微信、支付宝、银行卡 |
Spring JdbcTemplate 的 execute() | 折扣计算:无折扣、VIP、SVIP |
JUnit TestCase 的 setUp/test/tearDown | 排序算法:快排、归并、堆排 |
InputStream.read() 的骨架 | 压缩算法:zip、rar、7z |
| 数据导入:打开→读取→解析→转换→关闭 | 出行方式:打车、地铁、自驾 |
一句话区分:模板方法是"流程骨架我定好,你只填可变步骤";策略模式是"整个算法你随便换,我只负责调用"。
模板方法模式的优缺点
优点
1. 复用流程骨架,消除重复代码。 固定步骤只在父类写一次,所有子类共享,避免每个子类重复实现相同流程。
2. 符合开闭原则,扩展性好。 新增一种流程变体,通常只需新增一个子类,实现抽象方法、按需覆盖钩子,父类骨架不用改。
3. 控制流程顺序,防止子类破坏结构。 模板方法声明为 final,子类无法改变步骤顺序,保证算法骨架的稳定性。
4. 钩子提供灵活扩展点。 子类可以在不重写整个流程的前提下,按需介入某些可选步骤。
5. 符合好莱坞原则。 “不要调用我,让我调用你”——父类控制流程,子类只提供实现,降低子类对流程的掌控负担。
6. 代码结构清晰,职责分明。 流程在父类一目了然,可变步骤在子类各司其职,阅读和维护成本低。
缺点
1. 继承导致强耦合。 子类与父类紧密绑定,父类骨架变更可能影响所有子类;子类也难以脱离父类单独复用。
2. 类数量会增加。 每种流程变体一个子类,变体一多,类的数量明显上升。
3. 不支持运行时切换。 实例化哪个子类,就固定走哪套步骤,无法在执行时动态替换流程实现(这正是策略模式的强项)。
4. 流程骨架变更成本高。 一旦骨架需要调整(增删步骤、改顺序),所有子类都可能受影响,甚至需要修改抽象方法签名。
5. 钩子滥用会削弱骨架约束。 钩子给得太多、太细,子类就能通过钩子绕过流程约束,模板方法"锁流程"的价值被稀释。
6. 受继承层次限制。 Java 单继承,子类若已继承其他类,就无法再继承模板方法抽象类;C++ 多继承虽可行,但会带来菱形继承等复杂性。
7. 测试需要覆盖父类骨架与子类步骤。 父类骨架要测流程顺序,子类要测可变步骤,测试成本比普通类略高。
一个反面例子
“不要为了模式而模式”,这里给一个具体的对照:
// 场景:只有 2 个步骤,且流程几乎不变
// 写法 A:直接写两个方法 —— 2 个方法,15 行
void makeCoffee() {
boilWater();
brewCoffee();
pourInCup();
addSugarAndMilk();
}
void makeTea() {
boilWater();
brewTea();
pourInCup();
addLemon();
}
// 写法 B:引入模板方法模式 —— 1 抽象类 + 2 子类 + 1 客户端
// 收益:新增饮品时不改骨架;
// 成本:文件数从 1 变 4,新人需先理解继承体系才能改一行逻辑。
判断标准:
- 流程步骤 ≥ 3 且多个变体共享骨架 → 模板方法值得;
- 流程只有 2~3 步、变体少、几乎不变 → 直接写方法更直观;
- 流程固定但可变步骤多、且需要运行时切换 → 考虑模板方法 + 策略模式组合;
- 流程不固定、整个算法可替换 → 策略模式更合适。
应用场景
以下场景是模板方法模式常用的地方,但未必只能用模板方法模式。判断时看两条:
① 是否存在固定的流程骨架;
② 是否有多个变体共享这个骨架,只是部分步骤不同。
如果整个算法都可替换,优先考虑策略模式;如果只需要创建对象,考虑工厂方法(它本身就是模板方法的一种特例)。
主流框架使用模板方法模式的场景
Java Servlet 的 HttpServlet。 service() 方法是模板方法,它根据请求方法把请求分派给 doGet()、doPost()、doPut()、doDelete() 等。service() 的流程骨架(解析请求 → 分派 → 处理)由 Servlet 容器控制,开发者只需继承 HttpServlet 并实现对应的 doXxx() 方法。
Spring 的 JdbcTemplate。 execute()、query()、update() 等方法都是模板方法:获取连接 → 创建 Statement → 执行 → 处理结果 → 释放资源,这一整套流程由 JdbcTemplate 固定,开发者只需提供 PreparedStatementSetter、RowMapper、ResultSetExtractor 等回调,专注 SQL 与结果映射。
Spring 的 AbstractApplicationContext.refresh()。 refresh() 是典型的模板方法,定义了容器启动的骨架:准备上下文 → 获取 BeanFactory → 加载 BeanDefinition → 后置处理 → 注册监听器 → 实例化单例 Bean → 完成刷新。其中若干步骤是抽象方法或钩子,由子类(如 ClassPathXmlApplicationContext、AnnotationConfigApplicationContext)实现。
Java InputStream / OutputStream。 InputStream.read(byte[], int, int) 是模板方法,骨架是"循环调用 read() 直到读满或结束",子类只需实现 read() 这个抽象方法。OutputStream.write(byte[], int, int) 同理。
JUnit 的 TestCase(JUnit 3)。 runBare() 是模板方法,骨架是 setUp() → testXxx() → tearDown(),开发者只需实现测试方法和可选的 setUp/tearDown。JUnit 4/5 用注解替代了继承,但思路一脉相承。
Android Activity 生命周期。 onCreate()、onStart()、onResume()、onPause()、onStop()、onDestroy() 的调用顺序由框架控制,开发者按需覆盖,本质上是模板方法思想。
其他场景
数据导入与导出。 "打开文件 → 读取内容 → 解析 → 转换为领域对象 → 关闭资源"是固定骨架,解析步骤因 Excel、CSV、JSON、XML 而异。用模板方法固定骨架,子类只实现解析。
支付流程。 "参数校验 → 风控检查 → 扣款 → 记录流水 → 发送通知"是常见骨架,扣款步骤因渠道而异。可以用模板方法固定流程,用策略模式处理扣款渠道,两者组合。
文档生成。 "收集数据 → 渲染模板 → 填充内容 → 导出文件"是固定流程,渲染引擎(Freemarker、Thymeleaf、Word 模板)因格式而异。
游戏主循环。 "初始化 → 循环(输入 → 更新 → 渲染)→ 清理"是经典骨架,具体更新与渲染逻辑由子类或回调实现。
构建工具的生命周期。 Maven 的 clean、compile、test、package、install 是固定阶段,具体插件实现各阶段行为。
注意:如果"整个算法"都可替换、且需要运行时切换,这更接近策略模式;如果只是"创建对象"这一步可变,工厂方法就是模板方法的特例,不必再套一层模板方法。
审批流程。 "提交 → 初审 → 复审 → 终审 → 归档"是固定骨架,每个审批节点的具体规则因业务而异。用模板方法固定节点顺序,子类实现各节点规则。
测试执行框架。 "准备环境 → 执行测试 → 断言 → 清理环境"是固定骨架,具体测试逻辑由子类或回调提供。
注意事项
必读:模板方法与骨架设计
1. 模板方法应声明为 final。 这是模板方法模式的关键约束。如果允许子类覆盖模板方法,流程骨架就可能被篡改,"固定流程"的价值不复存在。C++ 用 final 关键字,Java 用 final 修饰符。
2. 抽象方法只放真正必须由子类实现的步骤。 不要把固定步骤也声明为抽象方法,否则子类被迫重复实现相同逻辑,违背复用初衷。
3. 钩子方法要克制。 钩子是扩展点,不是"万能开关"。给太多钩子,子类就能绕过流程约束;给太少,扩展性不足。钩子的默认实现要合理,且要写清楚"覆盖后会发生什么"。
4. 流程骨架要稳定。 模板方法一旦被大量子类依赖,增删步骤、调整顺序的成本很高。设计时要考虑清楚:哪些步骤是真正固定的,哪些是可能变化的,是否要给未来留扩展点。
5. 抽象类的职责要单一。 一个抽象类只应定义一个流程骨架。把多个不相关的流程塞进同一个抽象类,会导致子类被迫实现不需要的抽象方法。
必读:继承与耦合
6. 子类与父类是强耦合关系。 父类骨架变更会影响所有子类,父类的 protected 成员也会暴露给子类。设计时要控制 protected 成员的可见范围,避免子类过度依赖父类内部细节。
7. 注意里氏替换原则。 子类覆盖钩子或具体方法时,不应改变父类约定的语义。比如钩子 customerWantsCondiments() 默认返回 true,子类覆盖后不应产生与"是否加调料"无关的副作用。
8. Java 单继承的限制要提前考虑。 如果具体类已经继承了其他基类,就无法再继承模板方法抽象类。此时可以考虑用组合 + 函数式回调替代继承(见「函数式轻量变体」),或者用接口默认方法模拟。
9. C++ 多继承要谨慎。 模板方法抽象类作为基类时,注意虚继承、菱形继承和析构函数的虚化,避免资源泄漏和歧义。
必读:钩子与扩展点
10. 钩子方法的命名要能表达"是否/要不要"的语义。 比如 customerWantsCondiments()、shouldSkipValidation()、isLoggingEnabled(),一看就知道是可选开关。
11. 钩子的默认行为要安全且合理。 默认实现应该是最常见、最保守的行为,子类覆盖时应能明确感知到行为变化。不要让钩子的默认值成为"隐藏地雷"。
12. 钩子不要替代抽象方法。 如果某个步骤是流程必需的、且没有合理默认值,应该声明为抽象方法强制子类实现,而不是用钩子"可选覆盖"。
进阶:工程落地
13. 模板方法与策略模式可以组合使用。 流程骨架固定用模板方法,其中某个可变步骤用策略模式支持运行时切换。比如支付流程用模板方法固定"校验→扣款→记录→通知","扣款"用策略模式支持多渠道。
14. 模板方法要与工厂方法区分。 工厂方法本身就是模板方法的一种特例(创建对象这一步可变)。如果只是"创建对象"可变,用工厂方法即可,不必再套一层模板方法。
15. 框架扩展点优先用钩子,而不是抽象方法。 框架给使用者的扩展点,尽量设计成钩子(有默认实现),降低使用者的实现负担;只有真正必须实现的步骤才用抽象方法。
16. 注意模板方法的调用栈深度。 模板方法 → 抽象方法 → 子类实现,调用栈比普通方法深一层。在性能敏感场景(如高频调用的解析器)要评估开销。
17. 序列化场景要小心。 如果抽象类或子类需要序列化,注意 final 模板方法、protected 成员、钩子状态的序列化兼容性。跨版本反序列化时,骨架变更可能导致兼容问题。
进阶:可维护性与测试
18. 不要为了模式而模式。 如果流程只有两三步、变体少、几乎不变,直接写方法更直观(见「一个反面例子」)。
19. 警惕子类爆炸。 每种流程变体一个子类,变体一多类就多。可以通过合并真正相近的变体、用钩子参数化差异、用组合替代继承来缓解。
20. 抽象类命名要表达"流程"而非"实现"。 比如 Beverage、DataImporter、PaymentFlow,而不是 AbstractBeverageImpl、BaseHandler。
21. 文档要写清楚每个抽象方法和钩子的契约。 子类实现时需要知道:方法何时被调用、参数含义、返回值语义、是否允许抛异常、钩子覆盖后的影响。
22. 模板方法要单独测试流程顺序。 用一个测试子类记录各步骤调用顺序,断言模板方法确实按约定顺序执行。子类的抽象方法实现也要独立测试。
23. 钩子的分支要覆盖测试。 钩子返回 true / false 两条路径都要测,避免默认分支之外的路径长期无人覆盖。
24. 异常处理策略要统一。 抽象方法抛出的异常类型应在抽象类层约定,模板方法可以统一捕获、包装或转换,避免子类各自抛不同异常导致调用方难以处理。
模板方法模式不负责什么
把职责误加给抽象类或模板方法,是模板方法模式被用坏的最常见原因。明确边界,比记住定义更重要:
| 职责 | 谁负责 | 不该由谁负责 |
|---|---|---|
| 选择哪个具体子类 | 客户端、工厂、DI 容器 | 抽象类(抽象类不知道子类) |
| 运行时切换流程实现 | 不支持;需要切换请用策略模式 | 模板方法(实例化即固定) |
| 动态改变流程骨架 | 修改抽象类,重新编译 | 子类(模板方法应 final) |
| 步骤之间的数据传递 | 方法参数、返回值、受保护的成员字段 | 模板方法(只负责按顺序调用) |
| 子类步骤的异常兜底 | 子类自身或模板方法统一包装 | 抽象方法(应保持契约清晰) |
| 钩子的业务语义 | 子类覆盖时自行保证 | 抽象类(只提供默认行为) |
| 创建具体子类实例 | 客户端、工厂、DI 容器 | 抽象类(除非它同时是工厂) |
一句话:模板方法模式只做一件事——把"流程骨架"从"步骤实现"里分离出来,让骨架复用、步骤可变。其余的事情,交给客户端、工厂、策略模式或业务层。
总结
- 流程骨架固定,可变步骤下放子类;模板方法锁顺序,抽象方法填步骤,钩子留扩展。
- 接口定契约、抽象类定骨架、子类填步骤、客户端一次调用。
- 是否值得用,看两条:流程是否固定且多变体共享;可变部分是否只占少数步骤。二者缺一,先写普通方法。
- 落地时记住三件事:模板方法
final、钩子要克制、运行时切换请找策略模式。
179



被折叠的 条评论
为什么被折叠?



