模板方法模式练习

模板方法模式练习——饮料、数据读取与报告生成

按《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(),只实现了 brewadd_condiments;流程完全由父类 Beverage.prepare() 决定。

练习二:数据读取(打开 → 读 → 关闭)

目的

流程“打开 → 读内容 → 关闭”在父类模板方法里只写一次;子类只实现 openreadclose 三个步骤,不重写整体流程(含异常时也要关闭)。

要求

  • 定义抽象类 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;子类实现三部分;拼接顺序在父类。

自检问题

  1. 模板方法抽象步骤分别由谁写?
    模板方法(如 prepare、read_all、generate)在父类里写,规定步骤顺序;抽象步骤(如 brew、add_condiments、open、read、close)由子类实现,父类只声明不实现。

  2. 什么是钩子方法?和抽象方法有什么不同?
    钩子是父类里已有默认实现的方法,子类可以选填覆盖(如 want_condiments 默认 True,子类可改为 False);抽象方法没有默认实现,子类必须实现。钩子用于“可选步骤”或“条件步骤”。

  3. 子类可以重写模板方法吗?
    语法上可以,但设计上通常不重写模板方法,否则流程就不统一了;子类只实现/覆盖步骤,保证“骨架”在父类、细节在子类。

做完以上三道练习,再对照《模板方法模式》文档中的示例,对模板方法模式会掌握得比较扎实。建议每道题先自己写一遍,再对照参考答案和验证要点检查。

发表评论