Python 设计模式——模板方法模式(Template Method Pattern)详解
本文面向零基础新手,从概念到“骨架与步骤”,从抽象方法到钩子方法,全方位讲解模板方法模式。
一、什么是模板方法模式?
1.1 通俗理解
模板方法模式是一种行为型设计模式,核心思想是:
在父类里定义一个“模板方法”,用固定顺序写出算法的骨架(先做 A,再做 B,再做 C);其中某些步骤是抽象的或可选的,由子类去实现或覆盖。这样“流程”在父类里只写一遍,子类只关心“自己那几步”怎么实现,不改变整体顺序,既避免重复代码,又保证流程一致。
可以这样类比:
- 做茶和做咖啡:流程都是“烧水 → 冲泡 → 倒进杯子 → 加调料”。烧水、倒进杯子是一样的;但“冲泡”和“加调料”不同:茶是泡茶叶、加柠檬,咖啡是冲咖啡粉、加奶糖。父类把流程写成“模板方法”:先烧水,再调“冲泡”(子类实现),再倒杯,再调“加调料”(子类实现)。流程固定,步骤中的差异由子类填。
- 读数据:从文件读和从网络读,流程都是“打开 → 读内容 → 关闭”。只有“打开”和“读”的具体实现不同,“关闭”可能也不同。父类写一个“读数据”的模板方法,里面依次调 open、read、close,其中 open/read 由子类实现。骨架在父类,细节在子类。
所以,模板方法模式解决的是:多个子类有相同流程、只有部分步骤不同时,把流程提到父类的“模板方法”里,把不同的步骤留给子类实现,避免重复、保证顺序一致。
1.2 为什么需要模板方法模式?
场景一:流程相同、部分步骤不同
例如多种“数据导入”:都是“打开连接 → 读一批 → 处理 → 再读 → 关闭”,只有“打开”“读”的具体实现因数据源不同而不同。若每个子类都自己写一整遍流程,会重复且容易漏步骤。用模板方法:父类写流程,子类只实现“打开”“读”等步骤。
场景二:保证流程顺序与约束
父类可以规定“必须先验证再保存”“先打开再读再关闭”,子类不能打乱顺序,只能填具体实现。这样流程的约束在一处维护。
场景三:复用公共步骤、减少重复
相同步骤(如烧水、倒杯)只在父类写一次;子类只写差异步骤(如冲泡、加料),减少重复代码。
1.3 适用场景(什么时候用?)
| 场景 | 说明 |
|---|---|
| 多个类有相同算法骨架、部分步骤不同 | 如多种数据读取、多种报告生成、多种安装流程。 |
| 要固定流程顺序 | 父类规定先 A 后 B 后 C,子类不能颠倒。 |
| 希望子类只实现“变”的部分 | 公共逻辑在父类,差异逻辑在子类。 |
简单记忆:流程一样、步骤中有的要子类各自实现时,把流程写成模板方法,步骤用抽象方法或钩子交给子类。
二、模板方法模式的结构(两个角色)
| 角色 | 说明 |
|---|---|
| 抽象类(Abstract Class) | 定义模板方法(如 run、execute),里面按顺序调用多个步骤;步骤可以是抽象方法(子类必须实现)或具体方法(子类可覆盖也可不覆盖,即钩子)。 |
| 具体类(Concrete Class) | 继承抽象类,实现抽象步骤(或覆盖钩子),不重写模板方法本身(或只在不改顺序的前提下扩展)。 |
关系可以理解为:
- 模板方法 = 一个“总控”方法,内部:step1(); step2(); step3(); …
- step1、step2 等在父类中要么是抽象方法(子类必须实现),要么是已实现的方法(子类可选覆盖,即钩子)。
- 子类只实现/覆盖这些步骤,不重写模板方法的调用顺序,从而保证流程一致。
三、示例一:茶与咖啡(最经典)
需求:做茶和做咖啡的流程都是“烧水 → 冲泡 → 倒杯 → 加调料”,其中烧水、倒杯相同,冲泡和加调料不同。用模板方法固定流程,子类只实现“冲泡”和“加调料”。
3.1 抽象类:定义模板方法 + 步骤
from abc import ABC, abstractmethod
class Beverage(ABC):
"""抽象类:定义“冲饮料”的模板方法及步骤"""
def prepare(self):
"""模板方法:固定流程"""
self.boil_water()
self.brew()
self.pour_in_cup()
self.add_condiments()
def boil_water(self):
"""公共步骤:烧水"""
print(" 烧水")
@abstractmethod
def brew(self):
"""抽象步骤:冲泡,子类必须实现"""
pass
def pour_in_cup(self):
"""公共步骤:倒进杯子"""
print(" 倒进杯子")
@abstractmethod
def add_condiments(self):
"""抽象步骤:加调料,子类必须实现"""
pass
3.2 具体类:茶、咖啡
class Tea(Beverage):
def brew(self):
print(" 泡茶叶")
def add_condiments(self):
print(" 加柠檬")
class Coffee(Beverage):
def brew(self):
print(" 冲咖啡粉")
def add_condiments(self):
print(" 加奶和糖")
3.3 使用
Tea().prepare()
# 烧水 / 泡茶叶 / 倒进杯子 / 加柠檬
Coffee().prepare()
# 烧水 / 冲咖啡粉 / 倒进杯子 / 加奶和糖
要点:流程(prepare)只在 Beverage 里写一次;子类只实现 brew 和 add_condiments,不重写 prepare,保证顺序一致。
四、示例二:钩子方法(可选覆盖)
有时某一步不是“必须子类实现”,而是“子类可以选填”。父类给一个默认实现,子类需要时再覆盖,这类方法叫钩子(hook)。
例如:咖啡默认加调料,茶也加;但若有一种“纯黑咖啡”不加调料,可以在子类里覆盖 add_condiments 为空,或父类提供一个“是否加调料”的钩子,子类覆盖返回 False 则跳过加调料。
class Beverage(ABC):
def prepare(self):
self.boil_water()
self.brew()
self.pour_in_cup()
if self.want_condiments(): # 钩子:子类可覆盖
self.add_condiments()
def want_condiments(self) -> bool:
"""钩子方法:默认加调料,子类可覆盖为 False"""
return True
@abstractmethod
def add_condiments(self):
pass
class BlackCoffee(Coffee):
def want_condiments(self) -> bool:
return False
def add_condiments(self):
pass # 不会被执行到,因为 want_condiments 为 False
要点:want_condiments 是钩子,父类有默认实现(True),子类 BlackCoffee 覆盖为 False,模板方法里根据钩子决定是否执行 add_condiments。
五、示例三:数据读取(打开 → 读 → 关闭)
需求:从文件读和从网络读,流程都是“打开 → 读内容 → 关闭”;只有“打开”和“读”的实现不同。父类定义模板方法 read_all(),子类实现 open、read、close(若 close 也不同可由子类覆盖)。
from abc import ABC, abstractmethod
class DataReader(ABC):
def read_all(self) -> str:
"""模板方法:打开 → 读 → 关闭"""
self.open()
try:
data = self.read()
return data
finally:
self.close()
@abstractmethod
def open(self):
pass
@abstractmethod
def read(self) -> str:
pass
@abstractmethod
def close(self):
pass
class FileReader(DataReader):
def __init__(self, path: str):
self.path = path
self._f = None
def open(self):
self._f = open(self.path, "r", encoding="utf-8")
print(" 打开文件")
def read(self) -> str:
content = self._f.read()
print(" 读取文件内容")
return content
def close(self):
if self._f:
self._f.close()
print(" 关闭文件")
# 使用(需要真实文件时 path 指向实际路径)
# reader = FileReader("test.txt")
# print(reader.read_all())
要点:read_all 的流程(含 try/finally 关闭)只在父类写一次;FileReader 只实现 open/read/close,不重写 read_all。
六、示例四:简单报告生成(生成标题 → 生成正文 → 生成结尾)
需求:文本报告和 HTML 报告,流程都是“生成标题 → 生成正文 → 生成结尾”,只有每部分的格式不同。父类定义模板方法 generate(),子类实现 header、body、footer。
class Report(ABC):
def generate(self) -> str:
"""模板方法"""
parts = []
parts.append(self.header())
parts.append(self.body())
parts.append(self.footer())
return "n".join(parts)
@abstractmethod
def header(self) -> str:
pass
@abstractmethod
def body(self) -> str:
pass
@abstractmethod
def footer(self) -> str:
pass
class TextReport(Report):
def header(self) -> str:
return "=== 报告标题 ==="
def body(self) -> str:
return "正文内容"
def footer(self) -> str:
return "--- 结束 ---"
class HtmlReport(Report):
def header(self) -> str:
return "<h1>报告标题</h1>"
def body(self) -> str:
return "<p>正文内容</p>"
def footer(self) -> str:
return "<hr/><p>结束</p>"
print(TextReport().generate())
print(HtmlReport().generate())
七、模板方法 vs 策略模式
| 对比 | 模板方法 | 策略模式 |
|---|---|---|
| 实现方式 | 继承:子类实现/覆盖步骤 | 组合:上下文持有策略对象 |
| 流程 | 流程在父类固定,子类填步骤 | 流程由上下文调策略,可灵活 |
| 适用 | 流程一致、步骤可替换 | 算法整体可替换、不强调固定流程 |
简单记忆:模板方法 = 继承 + 固定流程 + 子类填步骤;策略 = 组合 + 可替换算法。
八、常见问题与注意点
8.1 子类能不能重写模板方法?
可以,但通常只在不破坏“固定流程”的前提下做扩展(如在前后加日志);若子类完全改掉流程,就失去了模板方法的意义。设计时尽量让“流程”稳定,只把“会变”的步骤留给子类。
8.2 抽象步骤多一点好还是少一点好?
步骤划分到“子类真正有差异”的粒度即可;若某步所有子类都一样,就放在父类里实现,不必抽象。抽象太多子类难写,抽象太少又重复。
8.3 钩子方法的命名
常见用 want_xxx、should_xxx、before_xxx、after_xxx 等表示“可选覆盖”的钩子,便于阅读。
九、小结
- 模板方法模式:在抽象类中定义模板方法(固定流程),流程中调用的步骤由子类实现(抽象方法)或可选覆盖(钩子);流程只写一次、顺序一致,子类只填差异步骤。
- 两个角色:抽象类(模板方法 + 步骤声明/默认实现)、具体类(实现抽象步骤、可选覆盖钩子)。
- 典型用途:冲饮料、数据读取、报告生成、安装流程等“流程相同、部分步骤不同”的场景。
- 实现要点:模板方法只调步骤、不写分支流程;抽象步骤子类必须实现;钩子提供默认实现,子类按需覆盖。
建议先写“茶与咖啡”的 prepare 流程,再试“数据读取”或“报告生成”,体会“骨架在父类、步骤在子类”,这样对模板方法模式会掌握得比较扎实。