模板方法 + 策略模式:用一杯奶茶讲透(Python / C++ 双实现)
一句话分工:
模板方法管「什么时候做」,策略管「怎么做」。
很多人学设计模式时,背得出定义,却在真实代码里认不出来。这篇文章换个思路:先讲一家奶茶店怎么干活,再落到代码,最后给你 Python 和 C++ 两套完整可运行的实现。
读完你应该能做到三件事:
- 拿到一段陌生代码,能一眼指出哪里是模板、哪里是策略;
- 判断一个新需求该"加类"还是"改流程";
- 知道 Python 和 C++ 在实现这两个模式时的关键差异。
一、先说结论
两个模式经常一起出现,因为它们的职责正好互补:
| 模板方法(Template Method) | 策略(Strategy) | |
|---|---|---|
| 回答什么问题 | 什么时候做、按什么顺序做 | 这一步具体怎么做 |
| 靠什么复用 | 继承(父类定骨架,子类填空) | 组合(持有一个可替换的算法对象) |
| 复用的是 | 顺序 | 算法 |
| 能否运行时替换 | 不能(类定义时就定死了) | 能 |
| GoF 分类 | 类行为型(Class behavioral) | 对象行为型(Object behavioral) |
什么时候两个一起用? 当同一块代码里存在两个相互独立的变化维度时:
- 维度 A:场景 / 来源 / 类型(堂食店、外卖店)→ 用继承(这是对象的"身份")
- 维度 B:某个环节的算法(红茶、绿茶、乌龙、咖啡)→ 用组合 / 策略(需要自由搭配)
一个维度用继承、一个用组合,类数量从 M × N 降到 M + N。
最实用的判据(可以直接对着代码用)
self.xxx()→ 属于模板 (流程自己控制)
参数.xxx()→ 属于策略(行为从外面传进来)
下文会反复用到这一条。
二、痛点:为什么不能一路 if-else 或一路继承
奶茶店的真实情况是这样的:
- 流程永远是那几步(贴在墙上的 SOP):取杯 → 加茶底 → 加糖 → 摇匀 → 加小料 → 封盖 → 叫号
- 但茶底有 4 种、小料有 4 种 → 菜单上就是
4 × 4 = 16种组合
如果"每种组合写一个类":
| 珍珠 | 椰果 | 布丁 | 不加
--------------+------+------+------+------
红茶 | ✔ | ✔ | ✔ | ✔
绿茶 | ✔ | ✔ | ✔ | ✔
乌龙 | ✔ | ✔ | ✔ | ✔
咖啡 | ✔ | ✔ | ✔ | ✔ ← 16 个类
茶底加到 5 种就是 20 个类;再加一个"少冰/去冰"维度就是 40 个。这就是用继承表达两个变化维度的必然结果------类的笛卡尔积爆炸。
如果一路 if-else 呢?就是下面这样,每加一种格式/口味都要回来改这个函数,无法单独测试,流程也散落各处:
python
def make(order, tea, topping):
if tea == "红茶": ...
elif tea == "绿茶": ...
elif tea == "乌龙": ...
elif tea == "咖啡": ...
if topping == "珍珠": ...
# 每加一种,这里就长一节
正确的做法是把这个函数拆成两半 :顺序留在原地(模板),算法搬出去(策略)。
三、场景拆解:SOP 和菜单
把奶茶店的勾选项按"角色"分一分,你会发现它们其实是四类完全不同的东西:
| 勾选项 | 选项 | 角色 | 用什么表达 |
|---|---|---|---|
| 流程顺序 | 取杯→加茶底→加糖→摇匀→加小料→封盖→叫号 | 模板方法 | 继承(父类写死) |
| 茶底 | 红茶 / 绿茶 / 乌龙 / 咖啡 | 策略 | 组合(传对象) |
| 小料 | 珍珠 / 椰果 / 布丁 / 不加 | 策略 | 组合(传对象) |
| 糖度 | 全糖 / 半糖 / 无糖 | 参数(不是策略!) | 方法参数 |
| 门店类型 | 堂食 / 外卖 | 身份 | 继承 |
两个判断口诀:
- 糖度为什么不做成策略? 因为它只是数据 ,没有"不同的做法"。差异能用"一个值"说完 → 参数;背后是"一套不同的算法" → 策略。把糖度做成
SugarStrategy是新手最常见的过度设计。 - 门店类型为什么用继承而不是策略? 因为"我是外卖店"是这个对象的身份(is-a),不会在同一杯奶茶的制作过程中从堂食店变成外卖店。
四、Python 实现
4.1 完整源码
python
"""用「奶茶店」讲清 模板方法 + 策略 ------ 单文件可运行
运行:python python/naicha.py
奶茶店的真实样子:
流程永远是那几步(贴在墙上的 SOP):
取杯 -> 加茶底 -> 加糖 -> 摇匀 -> 加小料 -> 封盖 -> 叫号
但茶底有 4 种、小料有 4 种 ------ 菜单上就是 4 x 4 = 16 种组合。
如果"每种组合写一个类"(用继承表达两个维度):16 个类起步,
菜单加一款茶底(4 种变 5 种)就变 20 个类。
本文件:流程 1 个类 + 茶底 4 个 + 小料 4 个 + 门店 2 个 = 11 个类,而且随便加。
四个角色的分工(这是整份代码的灵魂):
模板方法(继承):管「先做什么、后做什么」 -> MilkTeaMachine.make()
策略 (组合):管「这一步具体怎么做」 -> TeaBase(茶底)、Topping(小料)
参数 :只是数据,别做成策略 -> 糖度 sugar
钩子 :可有可无的额外动作 -> after_topping()
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
# ===========================================================================
# 顾客点的一杯。注意 size / sugar 是【数据】,不是算法
# ===========================================================================
@dataclass(frozen=True)
class Order:
size: str = "大杯"
sugar: str = "半糖"
# ===========================================================================
# 策略 A:茶底 ------ 4 种不同的"泡法",所以是策略,不是参数
# ===========================================================================
class TeaBase(ABC):
name: str = "?"
@abstractmethod
def brew(self) -> str:
"""泡出这一份茶底。"""
class BlackTea(TeaBase):
name = "红茶"
def brew(self) -> str:
return "红茶包泡 3 分钟"
class GreenTea(TeaBase):
name = "绿茶"
def brew(self) -> str:
return "绿茶用 80 度水泡 2 分钟"
class Oolong(TeaBase):
name = "乌龙"
def brew(self) -> str:
return "乌龙用 90 度水泡 4 分钟"
class Coffee(TeaBase):
name = "咖啡"
def brew(self) -> str:
return "萃一份浓缩咖啡液 30ml"
# ===========================================================================
# 策略 B:小料 ------ 4 种不同的"加料动作",同样是策略
# ===========================================================================
class Topping(ABC):
name: str = "?"
@abstractmethod
def add(self) -> str:
"""加小料这个动作。"""
class Pearl(Topping):
name = "珍珠"
def add(self) -> str:
return "舀一勺珍珠"
class CoconutJelly(Topping):
name = "椰果"
def add(self) -> str:
return "加一份椰果"
class Pudding(Topping):
name = "布丁"
def add(self) -> str:
return "放一块布丁"
class NoTopping(Topping):
"""空对象:什么都不加。
有了它,模板里就不用写 `if 小料 is None` ------ 把"什么都不做"也写成一个策略,
流程代码就永远是平的。这是策略模式最常用的搭档技巧。
"""
name = "不加料"
def add(self) -> str:
return "(不加小料)"
# ===========================================================================
# 模板方法:奶茶机 ------ 只负责"流程",对红茶绿茶珍珠椰果一无所知
# ===========================================================================
class MilkTeaMachine(ABC):
"""奶茶机上的 SOP。员工不许自己改流程。"""
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if "make" in cls.__dict__:
raise TypeError(f"{cls.__name__}: 禁止覆写 make(),流程由奶茶机统一规定")
# ---------------------------------------------------------------- 模板方法
def make(self, order: Order, base: TeaBase, topping: Topping) -> str:
"""步骤顺序只写这一遍。茶底/小料是按次传进来的零件(策略)。"""
steps = [
self.take_cup(order), # 固定步骤
f"茶底:{base.brew()}", # 策略:怎么泡
self.add_sugar(order), # 固定步骤(糖度是参数)
self.shake(), # 固定步骤
f"小料:{topping.add()}", # 策略:怎么加
self.after_topping(), # 钩子:默认动作,子类可加码
self.seal(), # 固定步骤
self.call(order, base, topping), # 抽象步骤:各门店自己的事
]
return "\n".join(f" {i}. {s}" for i, s in enumerate(steps, 1))
# ---------------------------------------------------------------- 固定步骤
def take_cup(self, order: Order) -> str:
return f"取一个{order.size}"
def add_sugar(self, order: Order) -> str:
return f"加糖:{order.sugar}"
def shake(self) -> str:
return "摇匀 10 下"
def seal(self) -> str:
return "封盖、插吸管"
# ---------------------------------------------------------------- 钩子
def after_topping(self) -> str:
"""默认动作:擦一下杯口。想加码的子类覆写它(记得 super())。"""
return "擦一下杯口"
# ---------------------------------------------------------------- 抽象步骤
@abstractmethod
def call(self, order: Order, base: TeaBase, topping: Topping) -> str:
"""叫号:堂食喊桌号,外卖投柜子。这是门店的固有差异,必须实现。"""
class DineInMachine(MilkTeaMachine):
"""堂食店:只实现必须实现的那一个方法。"""
def call(self, order, base, topping) -> str:
return f"叫号:{order.size}{base.name}{topping.name} 好了,请到吧台取"
class DeliveryMachine(MilkTeaMachine):
"""外卖店:叫号方式不同(抽象方法),另外自己加了一道防倒工序(钩子)。"""
def after_topping(self) -> str:
return "套上防倒杯套"
def call(self, order, base, topping) -> str:
return f"外卖:装袋放入 {order.size} 取餐柜"
class FlagshipMachine(DineInMachine):
"""旗舰店:它"仍然是一家堂食店",所以继承堂食店(is-a),只把钩子加码。
注意区别:门店类型是"身份" -> 继承;茶底/小料是"算法" -> 策略。
"""
def after_topping(self) -> str:
return super().after_topping() + ",再套一个防倒杯套" # 在默认动作之上加码
# ===========================================================================
# 菜单:名字 -> 具体类。有了它,点单的人不需要认识任何具体类
# ===========================================================================
TEAS: dict[str, type[TeaBase]] = {
"红茶": BlackTea, "绿茶": GreenTea, "乌龙": Oolong, "咖啡": Coffee,
}
TOPPINGS: dict[str, type[Topping]] = {
"珍珠": Pearl, "椰果": CoconutJelly, "布丁": Pudding, "不加": NoTopping,
}
MACHINES: dict[str, type[MilkTeaMachine]] = {
"堂食": DineInMachine, "外卖": DeliveryMachine, "旗舰店": FlagshipMachine,
}
def order(size: str, sugar: str, tea: str, topping: str, where: str = "堂食") -> str:
"""点单入口:顾客只说名字,不关心背后是哪个类。"""
return MACHINES[where]().make(Order(size, sugar), TEAS[tea](), TOPPINGS[topping]())
if __name__ == "__main__":
print("=" * 68)
print("1) 一杯「大杯 / 半糖 / 乌龙 / 珍珠」的完整流程")
print("=" * 68)
print(order("大杯", "半糖", "乌龙", "珍珠"))
print("\n" + "=" * 68)
print("2) 换茶底、换小料、换门店 ------ 奶茶机代码一行都不用改")
print("=" * 68)
print(order("中杯", "无糖", "绿茶", "椰果", "外卖"))
print()
print(order("大杯", "全糖", "咖啡", "布丁", "旗舰店"))
print("\n" + "=" * 68)
print("3) 为什么用组合而不是继承?看菜单")
print("=" * 68)
for tea_name in TEAS:
print(f" {tea_name}: " + " / ".join(TOPPINGS))
print(f"\n 茶底 {len(TEAS)} 种 x 小料 {len(TOPPINGS)} 种 = {len(TEAS) * len(TOPPINGS)} 种组合,"
f"但只用了 {len(TEAS)} + {len(TOPPINGS)} 个小类。")
print(" 用继承表达?那是 16 个类起步,而且茶底加到 5 种就是 20 个。")
print("\n" + "=" * 68)
print("4) 员工想自己改流程?拦住")
print("=" * 68)
try:
class Sneaky(MilkTeaMachine):
def make(self, order, base, topping): # 绕过 SOP,自己乱来
return "我不管流程,直接倒杯水"
except TypeError as exc:
print(f" 已拦住:{exc}")
4.2 逐段拆解
① 策略接口:只回答"怎么做"
python
class TeaBase(ABC):
@abstractmethod
def brew(self) -> str:
"""泡出这一份茶底。"""
ABC+@abstractmethod是一份强制契约 :谁做茶底,谁就必须自己写brew,否则连对象都造不出来(TypeError: Can't instantiate abstract class ...)。- 接口极小------只有
brew()一行。接口越小,组合越自由。
小料同理,是第二个策略维度:
python
class Topping(ABC):
@abstractmethod
def add(self) -> str:
"""加小料这个动作。"""
② 模板方法:8 步顺序只写一遍
python
def make(self, order: Order, base: TeaBase, topping: Topping) -> str:
steps = [
self.take_cup(order), # 固定步骤
f"茶底:{base.brew()}", # 策略:怎么泡
self.add_sugar(order), # 固定步骤(糖度是参数)
self.shake(), # 固定步骤
f"小料:{topping.add()}", # 策略:怎么加
self.after_topping(), # 钩子:默认动作,子类可加码
self.seal(), # 固定步骤
self.call(order, base, topping), # 抽象步骤:各门店自己的事
]
请注意这里的分工:
self.take_cup(...)、self.add_sugar(...)、self.shake()、self.seal()------ 模板的一部分,流程自己控制;base.brew()、topping.add()------ 策略 ,base和topping是参数,行为从外面传进来;self.after_topping()------ 钩子(有默认实现,子类可选覆写);self.call(...)------ 抽象方法(子类必须实现)。
模板和策略其实在同一条语句里握手:
python
f"茶底:{base.brew()}"
# ↑ ↑
# 模板的活 策略的活
# "第2步要泡茶" "具体怎么泡"
③ 三种"变化点"的区别(最容易搞混的地方)
| 写法 | 谁在变 | 属于 | 机制 |
|---|---|---|---|
base.brew() |
传给 make() 的对象变 |
策略 | 组合 |
self.call() |
make() 所在类的子类变 |
抽象方法 | 继承 |
self.after_topping() |
同上,但可选 | 钩子 | 继承 |
它们都会让"结果不同",但只有第一种是策略。这也是为什么很多人认不出策略模式------它可能就藏在一次普通的方法调用里。
④ 给模板方法上锁
只要有一个子类顺手覆写了 make(),顺序立刻散回各个子类,设计当场归零。Java 用 final,Python 没有 final,用 __init_subclass__ 做等效守卫:
python
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if "make" in cls.__dict__:
raise TypeError(f"{cls.__name__}: 禁止覆写 make(),流程由奶茶机统一规定")
⑤ 空对象:把"什么都不加"也写成一个策略
python
class NoTopping(Topping):
name = "不加料"
def add(self) -> str:
return "(不加小料)"
有了它,模板里永远不用写 if topping is None,代码永远是平的。空对象是策略模式最实用的搭档技巧。
⑥ 菜单:注册表 + 工厂,别让 if-else 长回来
设计得再好,如果调用方这么写:
python
if tea == "红茶": base = BlackTea()
elif tea == "绿茶": base = GreenTea()
那前面的努力就白费了------变化点只是从"泡茶逻辑"搬到了"选择逻辑"。收口办法是注册表 (TEAS 字典)+ 工厂函数:
python
def order(size, sugar, tea, topping, where="堂食"):
return MACHINES[where]().make(Order(size, sugar), TEAS[tea](), TOPPINGS[topping]())
调用方只认识名字,不认识任何具体类。新增一款茶底,只需要加一个类 + 在字典里登记一行。
4.3 实跑输出
下面是本文生成时实际运行 python/naicha.py 抓下来的输出(不是手抄的):
text
====================================================================
1) 一杯「大杯 / 半糖 / 乌龙 / 珍珠」的完整流程
====================================================================
1. 取一个大杯
2. 茶底:乌龙用 90 度水泡 4 分钟
3. 加糖:半糖
4. 摇匀 10 下
5. 小料:舀一勺珍珠
6. 擦一下杯口
7. 封盖、插吸管
8. 叫号:大杯乌龙珍珠 好了,请到吧台取
====================================================================
2) 换茶底、换小料、换门店 ------ 奶茶机代码一行都不用改
====================================================================
1. 取一个中杯
2. 茶底:绿茶用 80 度水泡 2 分钟
3. 加糖:无糖
4. 摇匀 10 下
5. 小料:加一份椰果
6. 套上防倒杯套
7. 封盖、插吸管
8. 外卖:装袋放入 中杯 取餐柜
1. 取一个大杯
2. 茶底:萃一份浓缩咖啡液 30ml
3. 加糖:全糖
4. 摇匀 10 下
5. 小料:放一块布丁
6. 擦一下杯口,再套一个防倒杯套
7. 封盖、插吸管
8. 叫号:大杯咖啡布丁 好了,请到吧台取
====================================================================
3) 为什么用组合而不是继承?看菜单
====================================================================
红茶: 珍珠 / 椰果 / 布丁 / 不加
绿茶: 珍珠 / 椰果 / 布丁 / 不加
乌龙: 珍珠 / 椰果 / 布丁 / 不加
咖啡: 珍珠 / 椰果 / 布丁 / 不加
茶底 4 种 x 小料 4 种 = 16 种组合,但只用了 4 + 4 个小类。
用继承表达?那是 16 个类起步,而且茶底加到 5 种就是 20 个。
====================================================================
4) 员工想自己改流程?拦住
====================================================================
已拦住:Sneaky: 禁止覆写 make(),流程由奶茶机统一规定
重点看第 2 节:换了茶底、上了外卖、跑了旗舰店,那 8 步的顺序一个字没改。
五、验收:加功能该动哪里?
判断一个设计好不好,最直接的方法就是问"加需求时要改几个地方":
| 需求 | 要动的地方 | 模板改动 |
|---|---|---|
| 加一款"茉莉花茶" | 加 1 个 TeaBase 子类 + 注册 1 行 |
0 |
| 加一款"芋泥"小料 | 加 1 个 Topping 子类 + 注册 1 行 |
0 |
| 加一种"自提柜"门店 | 加 1 个 MilkTeaMachine 子类 |
0(策略也不动) |
| 改流程(比如封盖前先贴标签) | 只改 make() 里加一行 |
1 处,全体生效 |
最后两行就是这两个模式各自的价值:
- 加算法 → 不用碰流程 → 策略的功劳
- 改流程 → 只改一处,所有组合一起变 → 模板方法的功劳
六、C++ 实现
C++ 里没有 ABC、没有 @abstractmethod、没有 GC,但这两个模式的骨架一模一样:还是"抽象基类 + 纯虚函数 + 组合"。
6.1 完整源码(C++17)
cpp
// naicha.cpp ------ 模板方法 + 策略模式(奶茶店版),对应 Python 的 naicha.py
//
// 编译运行(任选其一):
// g++ -std=c++17 -O2 -Wall -Wextra -o naicha naicha.cpp && ./naicha
// clang++ -std=c++17 -O2 -Wall -Wextra -o naicha naicha.cpp && ./naicha
// cl /std:c++17 /EHsc /W4 /utf-8 naicha.cpp (Windows + MSVC -> naicha.exe)
// 注意 /utf-8:本文件含中文字面量,不加它 MSVC 会按系统代码页解释(C4819 或乱码)
//
// 只依赖标准库。
#include <cstddef>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
// ===========================================================================
// 角色分工(与 Python 版一一对应)
// 模板方法(继承):MilkTeaMachine::make() ------ 管"先做什么、后做什么"
// 策略 (组合):TeaBase / Topping ------ 管"这一步具体怎么做"
// 参数 :Order ------ 只是数据(糖度别做成策略)
// 钩子 :afterTopping() ------ 可选动作
// ===========================================================================
// ---------------------------------------------------------------------------
// 订单:纯数据。对应 Python 的 @dataclass(frozen=True)
// 这里用 struct + 默认成员初始值,C++17 下可以直接 Order{} 或 Order{"中杯","无糖"}
// ---------------------------------------------------------------------------
struct Order {
std::string size = "大杯";
std::string sugar = "半糖";
};
// ---------------------------------------------------------------------------
// 策略 A:茶底
// 抽象基类 ≈ Python 的 class TeaBase(ABC)
// 纯虚函数 = 0 ≈ Python 的 @abstractmethod
// override ≈ 让编译器帮你检查"到底有没有覆写上"
// ---------------------------------------------------------------------------
class TeaBase {
public:
virtual ~TeaBase() = default; // 有虚函数就要有虚析构
virtual std::string name() const = 0;
virtual std::string brew() const = 0; // 只声明"怎么泡"
};
class BlackTea : public TeaBase {
public:
std::string name() const override { return "红茶"; }
std::string brew() const override { return "红茶包泡 3 分钟"; }
};
class GreenTea : public TeaBase {
public:
std::string name() const override { return "绿茶"; }
std::string brew() const override { return "绿茶用 80 度水泡 2 分钟"; }
};
class Oolong : public TeaBase {
public:
std::string name() const override { return "乌龙"; }
std::string brew() const override { return "乌龙用 90 度水泡 4 分钟"; }
};
class Coffee : public TeaBase {
public:
std::string name() const override { return "咖啡"; }
std::string brew() const override { return "萃一份浓缩咖啡液 30ml"; }
};
// ---------------------------------------------------------------------------
// 策略 B:小料
// ---------------------------------------------------------------------------
class Topping {
public:
virtual ~Topping() = default;
virtual std::string name() const = 0;
virtual std::string add() const = 0;
};
class Pearl : public Topping {
public:
std::string name() const override { return "珍珠"; }
std::string add() const override { return "舀一勺珍珠"; }
};
class CoconutJelly : public Topping {
public:
std::string name() const override { return "椰果"; }
std::string add() const override { return "加一份椰果"; }
};
class Pudding : public Topping {
public:
std::string name() const override { return "布丁"; }
std::string add() const override { return "放一块布丁"; }
};
// 空对象模式:把"什么都不加"也写成一个策略,流程里就永远不用写 if
class NoTopping : public Topping {
public:
std::string name() const override { return "不加料"; }
std::string add() const override { return "(不加小料)"; }
};
// ---------------------------------------------------------------------------
// 模板方法:奶茶机
// ---------------------------------------------------------------------------
class MilkTeaMachine {
public:
virtual ~MilkTeaMachine() = default;
// ------------------------------------------------------------------
// 模板方法:8 步顺序只写这一遍
//
// final 的作用:子类一旦想覆写 make(),【编译期】直接报错。
// 这相当于 Python 里那句 __init_subclass__ 守卫,只是:
// Python -> 运行时抛 TypeError
// C++ -> 编译期就过不去(更早、更硬)
//
// 参数用 const 引用传策略:不拷贝、不持有、不负责生命周期。
// ------------------------------------------------------------------
virtual std::string make(const Order& order,
const TeaBase& base,
const Topping& topping) const final {
std::vector<std::string> steps;
steps.push_back(takeCup(order)); // 固定步骤
steps.push_back("茶底:" + base.brew()); // 策略:怎么泡
steps.push_back(addSugar(order)); // 固定步骤(糖度是数据)
steps.push_back(shake()); // 固定步骤
steps.push_back("小料:" + topping.add()); // 策略:怎么加
steps.push_back(afterTopping()); // 钩子
steps.push_back(seal()); // 固定步骤
steps.push_back(call(order, base, topping)); // 抽象步骤
std::string out;
for (std::size_t i = 0; i < steps.size(); ++i) {
out += " " + std::to_string(i + 1) + ". " + steps[i] + "\n";
}
return out;
}
protected:
// ---------------- 固定步骤:刻意不写 virtual ----------------
// 不写 virtual,子类即使定义同名函数也只是"隐藏",多态调用时不会生效。
// 比 Python 版更严格(Python 里这些方法其实可以被覆写)。
std::string takeCup(const Order& order) const { return "取一个" + order.size; }
std::string addSugar(const Order& order) const { return "加糖:" + order.sugar; }
std::string shake() const { return "摇匀 10 下"; }
std::string seal() const { return "封盖、插吸管"; }
// ---------------- 钩子:有自己的默认实现 ----------------
virtual std::string afterTopping() const { return "擦一下杯口"; }
// ---------------- 抽象步骤:子类必须实现 ----------------
virtual std::string call(const Order& order,
const TeaBase& base,
const Topping& topping) const = 0;
};
// 堂食店:只实现必须实现的那一个
class DineInMachine : public MilkTeaMachine {
protected:
std::string call(const Order& order,
const TeaBase& base,
const Topping& topping) const override {
return "叫号:" + order.size + base.name() + topping.name() + " 好了,请到吧台取";
}
};
// 外卖店:叫号方式不同(抽象方法),另外加了防倒工序(钩子)
class DeliveryMachine : public MilkTeaMachine {
protected:
std::string afterTopping() const override { return "套上防倒杯套"; }
std::string call(const Order& order, const TeaBase&, const Topping&) const override {
return "外卖:装袋放入 " + order.size + " 取餐柜";
}
};
// 旗舰店:它"仍然是一家堂食店",所以继承堂食店,只在钩子上加码
// 对应 Python 的 super().after_topping()
class FlagshipMachine : public DineInMachine {
protected:
std::string afterTopping() const override {
return DineInMachine::afterTopping() + ",再套一个防倒杯套";
}
};
// ---------------------------------------------------------------------------
// 菜单:名字 -> 造对象的工厂函数
// 对应 Python 里 dict[str, type] + 工厂函数 的那套注册表
// 用 unique_ptr 表达"这块内存归我管"
// ---------------------------------------------------------------------------
using TeaFactory = std::function<std::unique_ptr<TeaBase>()>;
using ToppingFactory = std::function<std::unique_ptr<Topping>()>;
const std::map<std::string, TeaFactory>& teaMenu() {
static const std::map<std::string, TeaFactory> menu{
{"红茶", [] { return std::make_unique<BlackTea>(); }},
{"绿茶", [] { return std::make_unique<GreenTea>(); }},
{"乌龙", [] { return std::make_unique<Oolong>(); }},
{"咖啡", [] { return std::make_unique<Coffee>(); }},
};
return menu;
}
const std::map<std::string, ToppingFactory>& toppingMenu() {
static const std::map<std::string, ToppingFactory> menu{
{"珍珠", [] { return std::make_unique<Pearl>(); }},
{"椰果", [] { return std::make_unique<CoconutJelly>(); }},
{"布丁", [] { return std::make_unique<Pudding>(); }},
{"不加", [] { return std::make_unique<NoTopping>(); }},
};
return menu;
}
std::unique_ptr<TeaBase> orderTea(const std::string& name) {
const auto& menu = teaMenu();
const auto it = menu.find(name);
if (it == menu.end()) {
throw std::invalid_argument("不支持的茶底: " + name);
}
return it->second();
}
std::unique_ptr<Topping> orderTopping(const std::string& name) {
const auto& menu = toppingMenu();
const auto it = menu.find(name);
if (it == menu.end()) {
throw std::invalid_argument("不支持的小料: " + name);
}
return it->second();
}
int main() {
try {
const Order big{"大杯", "半糖"};
const Order small{"中杯", "无糖"};
std::cout << "1) 一杯「大杯 / 半糖 / 乌龙 / 珍珠」的完整流程\n";
DineInMachine dineIn;
std::cout << dineIn.make(big, Oolong{}, Pearl{});
std::cout << "\n2) 换茶底、换小料、换门店 ------ 奶茶机代码一行都不用改\n";
DeliveryMachine delivery;
std::cout << delivery.make(small, GreenTea{}, CoconutJelly{});
FlagshipMachine flagship;
std::cout << flagship.make(big, Coffee{}, Pudding{});
std::cout << "\n3) 走菜单(注册表 + 工厂):点单的人不需要认识任何具体类\n";
const auto tea = orderTea("乌龙");
const auto topping = orderTopping("椰果");
std::cout << dineIn.make(small, *tea, *topping);
std::cout << "\n4) 菜单内容\n";
std::cout << " 茶底:";
for (const auto& kv : teaMenu()) std::cout << " " << kv.first;
std::cout << "\n 小料:";
for (const auto& kv : toppingMenu()) std::cout << " " << kv.first;
std::cout << "\n 组合数: " << teaMenu().size() << " x " << toppingMenu().size()
<< " = " << teaMenu().size() * toppingMenu().size() << " 种,"
<< "但只用了 " << teaMenu().size() << " + " << toppingMenu().size()
<< " 个策略小类\n";
std::cout << "\n5) 点一个菜单上没有的?抛异常拦住\n";
try {
const auto bad = orderTea("白开水");
std::cout << " 居然做出来了: " << bad->name() << "\n";
} catch (const std::exception& e) {
std::cout << " 已拦住: " << e.what() << "\n";
}
} catch (const std::exception& e) {
std::cerr << "出错了: " << e.what() << "\n";
return 1;
}
return 0;
}
// ---------------------------------------------------------------------------
// 想试试"子类偷改流程"会怎样?把下面这段贴到 main() 之前再编译,
// 编译器会直接拒绝。实测 GCC 13.1.0 的原话:
// error: virtual function 'virtual std::string Sneaky::make(const Order&, const TeaBase&, const Topping&) const' overriding final function
// note: overridden function is 'virtual std::string Machine::make(const Order&, const TeaBase&, const Topping&) const'
//
// class Sneaky : public MilkTeaMachine {
// public:
// std::string make(const Order&, const TeaBase&, const Topping&) const override {
// return "我不管流程,直接倒杯水";
// }
// // 纯虚的 call() 也必须实现,否则 Sneaky 自己也是抽象类
// std::string call(const Order&, const TeaBase&, const Topping&) const override {
// return "";
// }
// };
//
// 这就是模板方法的"锁":Python 靠 __init_subclass__ 在运行时报错,
// C++ 靠 final 在编译期报错 ------ 更早、更硬。
// ---------------------------------------------------------------------------
6.2 编译运行
bash
# GCC / MinGW
g++ -std=c++17 -O2 -Wall -Wextra -o naicha naicha.cpp && ./naicha
# Clang
clang++ -std=c++17 -O2 -Wall -Wextra -o naicha naicha.cpp && ./naicha
# Windows + MSVC(生成 naicha.exe)
# 注意 /utf-8:本文件含中文字面量,不加它 MSVC 会按系统代码页解释(C4819 或乱码)
cl /std:c++17 /EHsc /W4 /utf-8 naicha.cpp
Windows 上如果控制台显示中文乱码,先执行
chcp 65001切到 UTF-8 代码页(只影响显示,不影响程序逻辑)。GCC / Clang 没有这个问题。
实测环境:GCC 13.1.0(MinGW-w64, x86_64-posix-seh) ,-Wall -Wextra 下零警告。
踩坑提示(实测) :Windows 上如果按全路径 调用
g++.exe,却没有把它的bin目录加进PATH,会遇到一个很迷惑的现象------g++ --version能正常打印版本号,但一编译就静默失败 :退出码 1、stdout/stderr全是空的、也不生成目标文件。真实原因是编译器子进程
cc1plus.exe找不到同目录的 DLL(进程退出码0xC0000135=STATUS_DLL_NOT_FOUND)。把...\mingw64\bin加进PATH就好了。
6.3 实跑输出
下面是本文生成时实际编译并运行 cpp/naicha.cpp 抓下来的输出(不是手抄的):
text
1) 一杯「大杯 / 半糖 / 乌龙 / 珍珠」的完整流程
1. 取一个大杯
2. 茶底:乌龙用 90 度水泡 4 分钟
3. 加糖:半糖
4. 摇匀 10 下
5. 小料:舀一勺珍珠
6. 擦一下杯口
7. 封盖、插吸管
8. 叫号:大杯乌龙珍珠 好了,请到吧台取
2) 换茶底、换小料、换门店 ------ 奶茶机代码一行都不用改
1. 取一个中杯
2. 茶底:绿茶用 80 度水泡 2 分钟
3. 加糖:无糖
4. 摇匀 10 下
5. 小料:加一份椰果
6. 套上防倒杯套
7. 封盖、插吸管
8. 外卖:装袋放入 中杯 取餐柜
1. 取一个大杯
2. 茶底:萃一份浓缩咖啡液 30ml
3. 加糖:半糖
4. 摇匀 10 下
5. 小料:放一块布丁
6. 擦一下杯口,再套一个防倒杯套
7. 封盖、插吸管
8. 叫号:大杯咖啡布丁 好了,请到吧台取
3) 走菜单(注册表 + 工厂):点单的人不需要认识任何具体类
1. 取一个中杯
2. 茶底:乌龙用 90 度水泡 4 分钟
3. 加糖:无糖
4. 摇匀 10 下
5. 小料:加一份椰果
6. 擦一下杯口
7. 封盖、插吸管
8. 叫号:中杯乌龙椰果 好了,请到吧台取
4) 菜单内容
茶底: 乌龙 咖啡 红茶 绿茶
小料: 不加 布丁 椰果 珍珠
组合数: 4 x 4 = 16 种,但只用了 4 + 4 个策略小类
5) 点一个菜单上没有的?抛异常拦住
已拦住: 不支持的茶底: 白开水
可以看到输出和 Python 版逐行对应:同一套 8 步流程、同样的步骤编号,只是第 4 节多了一段菜单信息。
6.4 逐点对照
| 概念 | Python | C++ |
|---|---|---|
| 策略接口 | class TeaBase(ABC) |
抽象基类 class TeaBase |
| "必须实现" | @abstractmethod(运行时拦截) |
virtual ... = 0(编译期强制) |
| 子类实现 | def brew(self) |
std::string brew() const override |
| 覆写检查 | 无(签名写错也照跑) | override 让编译器帮你查 |
| 禁止改模板方法 | __init_subclass__ 运行时抛 TypeError |
virtual ... final 编译期报错 |
| 传策略 | 直接传对象(鸭子类型) | const TeaBase& 引用(不拷贝、不持有) |
| 策略的生命周期 | GC 管 | 谁 new 谁负责 → std::unique_ptr |
| 注册表 | dict[str, type] |
std::map<std::string, std::function<std::unique_ptr<TeaBase>()>> |
| 抽象方法能有实现吗 | 能,子类可 super() |
纯虚函数也能有定义(类外定义后可用 Base::f() 调),但一般不这么写 |
6.5 三个 C++ 特有的注意点
① final vs 运行时守卫:错误发现得更早
cpp
virtual std::string make(const Order& order,
const TeaBase& base,
const Topping& topping) const final
- Python:子类覆写
make()→ 运行时 (定义类那一刻)抛TypeError; - C++:子类覆写被标
final的make()→ 编译期直接报错。实测 GCC 13.1.0 的原话是:
text
error: virtual function 'virtual std::string Sneaky::make(const Order&, const TeaBase&, const Topping&) const' overriding final function
note: overridden function is 'virtual std::string Machine::make(const Order&, const TeaBase&, const Topping&) const'
一个在运行时、一个在编译期,但都达到了"流程不许被子类改"的目的。
值得一提的是:final 只能用在虚函数上。如果你干脆不写 virtual ,子类就算定义了同名函数也只是"名字隐藏",多态调用时根本不会生效------这是另一种(更隐蔽的)锁。本文的做法是显式 virtual ... final,让编译器把错误喊出来。
② 所有权必须自己说清楚
Python 里 machine.make(order, Oolong(), Pearl()) 用完就扔,剩下的交给 GC。C++ 里必须回答"这块内存谁负责":
- 模板方法只是"用一下"策略 → 传
const TeaBase&(引用),不拷贝、不持有、不负责生命周期。这是最清晰的表达。 - 策略需要活得更久、需要从菜单里造出来 → 用
std::unique_ptr明确"独享所有权",orderTea()返回工厂造出的对象,调用方拿*tea使用。
③ 有虚函数就要有虚析构
cpp
class TeaBase {
public:
virtual ~TeaBase() = default; // 少这一行,delete 基类指针就是未定义行为
Python 里不存在这个问题(GC 按真实类型回收),C++ 里是硬性规定。
6.6 进阶:C++ 还能用"静态多态"做策略
上面的写法是动态多态(虚函数):运行时可替换,代价是一次间接调用(vtable 查表)。
C++ 还有另一种选择------编译期绑定:
cpp
// 策略作为模板参数:编译期就确定,可内联,零虚函数开销
template <class Tea, class Top>
class Machine {
public:
std::string make(const Order& order) const {
return takeCup(order) + Tea{}.brew() + Top{}.add();
}
};
// 用法:Machine<Oolong, Pearl> m;
取舍很清楚:
| 动态多态(虚函数 / 本文实现) | 静态多态(模板参数) | |
|---|---|---|
| 何时绑定 | 运行时 | 编译期 |
| 能否运行时替换 | 能(菜单、配置、插件必须用它) | 不能 |
| 开销 | 一次间接调用,无法内联 | 可内联,零开销 |
| 编译时间 / 报错可读性 | 好 | 差一些,代码膨胀 |
判断标准还是那句老话:策略需要在运行时按名字/配置挑选 → 用虚函数;所有组合在编译期就已知且追求极致性能 → 用模板。Python 里没有这个选择(只有动态派发),这也是 C++ 实现同一个模式时最值得多想一步的地方。
七、什么时候不要用
- 只有两个分支、而且长期不变:
if-else更便宜也更好读。 - 只有一个变化维度:要么只用模板方法(差异是"这类东西的身份"),要么只用策略(没有固定流程可复用)。硬凑两个模式 = 过度设计。
- 团队里没人说得清"哪一步会变":先把需求问清楚,比套模式重要。
- 需要的是横切关注点(日志、重试、事务):那是装饰器 / AOP 的活,不是策略。
八、常见坑
- 模板方法被子类覆写 ------顺序又散回各处。Java 用
final,Python 用__init_subclass__守卫,C++ 用final。 - 在构造期调用可覆写方法 (
__init__里调self.render()):此时子类字段还没初始化。骨架要由外部显式调用(本文的make()就是显式调用,安全;Java 构造器里调抽象方法是经典事故)。 - 继承层级超过 2 层 :
CsvExporter → ApiCsvExporter → AuditedApiCsvExporter一旦出现,说明你在用继承表达变化维度,该换策略 / 装饰器了。 - 策略有状态却被复用:同一个策略实例并发跑两次,数据串台。策略尽量无状态;有状态就每次新建。
- 策略反向"拉"上下文 (策略里直接访问调用方内部)→ 双向依赖。正确做法是把需要的数据当参数推进去。
- 调用方还在
if-elif里 new 策略:变化点只是搬了个家。用注册表 + 工厂收口。 - 提前抽象:只有一个实现就抽策略 = 凭空多一个文件和一层跳转。等第三个分支出现再抽(Rule of Three)。
- 命名混乱 :模板方法的名字里不该出现具体算法(叫
exportCsv就说明还没抽象干净);钩子用before_*/after_*/on_*,一眼看出是可选的。
九、如何复现本文所有内容
目录结构:
template-strategy/
├── python/naicha.py # 本文的 Python 实现(第 4 节源码)
├── cpp/naicha.cpp # 本文的 C++ 实现(第 6 节源码)
├── check_env.py # 一键自检:Python 版本 + 所有示例能否运行
└── blog/
├── blog.template.md # 本文正文:源码和输出都直接写在里面,能直接读
├── build_blog.py # 同步脚本:原地刷新代码块 + 产出发布文件
└── 模板方法+策略-奶茶店版.md # ← 生成出来的这篇博客
跑一遍:
bash
python python/naicha.py # 单独看 Python 版
python check_env.py # 自检环境与全部示例
python blog/build_blog.py # 重新生成本文(代码与输出自动同步)
为什么还要一个同步脚本? 因为文章里的源码和输出都不是手抄的:build_blog.py 会读真实文件、真的运行 Python、真的编译并运行 C++ ,然后把结果原地刷新回 blog.template.md。
所以 blog.template.md 本身就是一篇可以直接阅读、直接发布的完整文章(代码就写在正文里,不是链接);而改完代码重跑一次脚本,文章里的代码和输出又会自动跟上------不会出现"文章里的代码和仓库不一致"这种经典尴尬。
十、结语
把这两个模式记成一句话就够了:
模板方法负责"安排顺序",策略负责"填内容"。
看代码时:
self.xxx()是模板,参数.xxx()是策略。
它们的价值不在于"用了什么模式",而在于把变化的边界画清楚了:
- 想知道"有哪些奶茶" → 看策略类;
- 想知道"奶茶是怎么做出来的" → 看
make()那 8 行; - 想知道"哪些东西可以换" → 看
make()的参数表。
一个类的参数表 ,就是它允许你改变的东西;一个类的内部顺序,就是它不允许你改变的东西。设计的好坏,往往就差在这条线的位置。