python设计模式–模板方法模式(Template Method Pattern)

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_xxxshould_xxxbefore_xxxafter_xxx 等表示“可选覆盖”的钩子,便于阅读。


九、小结

  • 模板方法模式:在抽象类中定义模板方法(固定流程),流程中调用的步骤由子类实现(抽象方法)或可选覆盖(钩子);流程只写一次、顺序一致,子类只填差异步骤。
  • 两个角色:抽象类(模板方法 + 步骤声明/默认实现)、具体类(实现抽象步骤、可选覆盖钩子)。
  • 典型用途:冲饮料、数据读取、报告生成、安装流程等“流程相同、部分步骤不同”的场景。
  • 实现要点:模板方法只调步骤、不写分支流程;抽象步骤子类必须实现;钩子提供默认实现,子类按需覆盖。

建议先写“茶与咖啡”的 prepare 流程,再试“数据读取”或“报告生成”,体会“骨架在父类、步骤在子类”,这样对模板方法模式会掌握得比较扎实。

发表评论