模板方法模式练习——饮料、数据读取与报告生成
按《Python 设计模式——模板方法模式》的建议,通过三道练习巩固:① 饮料冲泡流程(茶/咖啡);② 数据读取流程(打开→读→关闭);③ 报告生成流程(标题→正文→结尾)。每步都有完整可运行代码和验证要点。
练习一:饮料冲泡(茶与咖啡)
目的
体会模板方法在父类里固定流程(烧水→冲泡→倒杯→加调料),抽象步骤由子类实现(冲泡、加调料);子类不重写流程,只填“自己那几步”。
要求
- 定义抽象类 Beverage,包含:
- prepare()(模板方法):依次调用 boil_water()、brew()、pour_in_cup()、add_condiments();
- boil_water()、pour_in_cup() 为普通方法,内部只 print 一句话;
- brew()、add_condiments() 为抽象方法。
- 实现 Tea:brew 打印“泡茶叶”,add_condiments 打印“加柠檬”。
- 实现 Coffee:brew 打印“冲咖啡粉”,add_condiments 打印“加奶和糖”。
- 客户代码:分别调用 Tea().prepare() 和 Coffee().prepare(),验证输出顺序一致且步骤内容符合茶/咖啡。
参考答案
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
class Tea(Beverage):
def brew(self):
print(" 泡茶叶")
def add_condiments(self):
print(" 加柠檬")
class Coffee(Beverage):
def brew(self):
print(" 冲咖啡粉")
def add_condiments(self):
print(" 加奶和糖")
# 使用
Tea().prepare()
# 烧水 / 泡茶叶 / 倒进杯子 / 加柠檬
Coffee().prepare()
# 烧水 / 冲咖啡粉 / 倒进杯子 / 加奶和糖
验证要点
- Tea().prepare() 输出顺序为:烧水 → 泡茶叶 → 倒进杯子 → 加柠檬。
- Coffee().prepare() 输出顺序为:烧水 → 冲咖啡粉 → 倒进杯子 → 加奶和糖。
- 确认:Tea/Coffee 没有重写 prepare(),只实现了 brew 和 add_condiments;流程完全由父类 Beverage.prepare() 决定。
练习二:数据读取(打开 → 读 → 关闭)
目的
流程“打开 → 读内容 → 关闭”在父类模板方法里只写一次;子类只实现 open、read、close 三个步骤,不重写整体流程(含异常时也要关闭)。
要求
- 定义抽象类 DataReader,包含:
- read_all()(模板方法):先 open(),再在 try 里调 read() 并返回结果,finally 里调 close();
- open()、read()、close() 为抽象方法。
- 实现 StringReader(简单实现,不真开文件):open 时打印“打开”,read 返回字符串
"hello",close 时打印“关闭”。 - 客户代码:调用 StringReader().read_all(),验证返回
"hello"且控制台有“打开”“关闭”输出。
参考答案
from abc import ABC, abstractmethod
class DataReader(ABC):
def read_all(self) -> str:
"""模板方法:打开 → 读 → 关闭(含 finally)"""
self.open()
try:
return self.read()
finally:
self.close()
@abstractmethod
def open(self):
pass
@abstractmethod
def read(self) -> str:
pass
@abstractmethod
def close(self):
pass
class StringReader(DataReader):
def open(self):
print(" 打开")
def read(self) -> str:
return "hello"
def close(self):
print(" 关闭")
# 使用
r = StringReader()
result = r.read_all()
print(result) # hello,且先有“打开”、后有“关闭”
验证要点
- read_all() 返回 "hello";控制台先输出“打开”,再输出“关闭”。
- 确认:StringReader 没有重写 read_all();打开/读/关闭的顺序和 try-finally 只在 DataReader.read_all() 里写一次。
练习三:报告生成(标题 → 正文 → 结尾)
目的
“生成报告”的流程为:标题 → 正文 → 结尾,三者拼接成最终字符串;父类定义模板方法 generate(),子类只实现 header()、body()、footer() 三个步骤。
要求
- 定义抽象类 Report,包含:
- generate()(模板方法):返回 header() + body() + footer() 拼接的字符串(中间可加换行);
- header()、body()、footer() 为抽象方法,返回 str。
- 实现 TextReport:header 返回
"=== 标题 ===",body 返回"正文",footer 返回"--- 结束 ---"。 - 实现 HtmlReport:header 返回
"<h1>标题</h1>",body 返回"<p>正文</p>",footer 返回"<hr/>"。 - 客户代码:分别调用 TextReport().generate() 和 HtmlReport().generate(),验证返回字符串包含预期内容且顺序为标题→正文→结尾。
参考答案
from abc import ABC, abstractmethod
class Report(ABC):
def generate(self) -> str:
"""模板方法:标题 + 正文 + 结尾"""
return self.header() + "n" + self.body() + "n" + self.footer()
@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/>"
# 使用
print(TextReport().generate())
# === 标题 ===
# 正文
# --- 结束 ---
print(HtmlReport().generate())
# <h1>标题</h1>
# <p>正文</p>
# <hr/>
验证要点
- TextReport().generate() 的字符串中,标题、正文、结尾顺序正确且内容符合上述约定。
- HtmlReport().generate() 的字符串中包含
<h1>、<p>、<hr/>,顺序正确。 - 确认:TextReport/HtmlReport 没有重写 generate(),只实现了 header/body/footer;拼接顺序由 Report.generate() 固定。
三步汇总与自检
| 练习 | 重点 | 关键点 |
|---|---|---|
| 一 | 饮料 prepare 流程 | 模板方法 prepare 调 4 步;brew、add_condiments 抽象,子类实现;流程不变。 |
| 二 | 数据 read_all 流程 | 模板方法 open→try read→finally close;子类实现 open/read/close;流程与异常处理在父类。 |
| 三 | 报告 generate 流程 | 模板方法返回 header+body+footer;子类实现三部分;拼接顺序在父类。 |
自检问题
-
模板方法和抽象步骤分别由谁写?
模板方法(如 prepare、read_all、generate)在父类里写,规定步骤顺序;抽象步骤(如 brew、add_condiments、open、read、close)由子类实现,父类只声明不实现。 -
什么是钩子方法?和抽象方法有什么不同?
钩子是父类里已有默认实现的方法,子类可以选填覆盖(如 want_condiments 默认 True,子类可改为 False);抽象方法没有默认实现,子类必须实现。钩子用于“可选步骤”或“条件步骤”。 -
子类可以重写模板方法吗?
语法上可以,但设计上通常不重写模板方法,否则流程就不统一了;子类只实现/覆盖步骤,保证“骨架”在父类、细节在子类。
做完以上三道练习,再对照《模板方法模式》文档中的示例,对模板方法模式会掌握得比较扎实。建议每道题先自己写一遍,再对照参考答案和验证要点检查。