精通 Python 设计模式——行为型设计模式

74 阅读47分钟

在上一章中,我们介绍了有助于编写简洁、可维护、可扩展代码的结构型模式以及 OOP 相关模式。接下来要讲的是行为型设计模式。行为型模式关注对象之间的协作(连接)算法

本章将涵盖以下主题:

  • 责任链(Chain of Responsibility)模式
  • 命令(Command)模式
  • 观察者(Observer)模式
  • 状态(State)模式
  • 解释器(Interpreter)模式
  • 策略(Strategy)模式
  • 备忘录(Memento)模式
  • 迭代器(Iterator)模式
  • 模板(Template)模式
  • 其他行为型设计模式

读完本章后,你将学会如何用行为型模式改进软件项目的设计。

技术要求

请参阅第 1 章的通用要求。本章示例代码的附加依赖如下:

  • 状态模式 小节:安装 state_machine 模块
    python -m pip install state_machine
  • 解释器模式 小节:安装 pyparsing 模块
    python -m pip install pyparsing
  • 模板模式 小节:安装 cowpy 模块
    python -m pip install cowpy

责任链(Chain of Responsibility)模式

责任链模式提供了一种优雅的请求处理方式:将请求沿着一条处理者链依次传递。链上的每个处理者都可以自主决定处理该请求,还是将其继续传递给下一个处理者。对于涉及多个处理者、但不一定都要参与的场景,这个模式尤为出色。

在实践中,这个模式鼓励我们聚焦对象请求在应用内的流动。值得注意的是,客户端对整条处理链是不知情的;它只与链上的第一个处理节点交互。同样,每个处理节点只认识它的直接后继——这是一种单向关系,类似单向链表。这种结构是为了解耦**发送方(客户端)接收方(处理节点)**而刻意设计的。

现实世界示例

  • ATM(以及所有接收/吐出纸币或硬币的机器,如零食售货机)采用了责任链模式。通常只有一个投币/存钞口(可视为“公共通信介质”),而不同面额的接收仓可视为不同的处理节点。投进的纸币会被引导到对应仓位;取钱时也从相应仓位吐出。结果可能来自一个或多个仓位的组合。

image.png 图 5.1 —— 责任链示例:ATM(图片来源:sourcemaking.com)

  • 在某些 Web 框架里,过滤器/中间件会在 HTTP 请求到达目标之前依次执行:每个过滤器各司其职(用户认证、日志、压缩等),要么将请求转发至下一个过滤器直到链尾,要么在出现错误(例如连续三次认证失败)时中断流程。

适用场景

  • 使用责任链模式,可以让多个不同对象都有机会满足同一请求。当我们事先不知道由谁来处理一个请求时,这尤其有用。采购系统就是例子:不同审批人对应不同的审批额度。假设某审批人最多可批 100,超过就交给链上的下一个审批人(最多100,超过就交给链上的下一个审批人(最多 200),以此类推。
  • 当我们知道可能有不止一个对象需要处理同一请求时也很适用,这在事件驱动编程中很常见:单个事件(如鼠标左键点击)可能被多个监听器捕获。
  • 如果所有请求都能由某一个处理者处理(且我们清楚是哪一个),那么责任链的价值不大。该模式的价值主要在其提供的解耦(参见第 1 章《基础设计原则》的“低耦合”一节):相比“客户端与所有处理者多对多相连”,客户端只需知道如何与链头通信。

实现责任链模式

在 Python 中实现责任链有多种方式。这里采用我最喜欢的实现之一——Vespe Savikko 的 Pythonic 动态分发思路(legacy.python.org/workshops/1…)。

我们以此为指南,实现一个简易的事件驱动系统。下图为系统的 UML 类图:

image.png 图 5.2 —— 事件驱动窗口系统的 UML 类图

Event 类描述一个事件。为简单起见,这里事件只有一个 name 属性:

class Event:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return self.name

Widget 是应用的核心类。UML 中的父子聚合关系表示每个控件可引用一个父对象(按约定也是 Widget 实例)。当然,任何 Widget 的子类实例(如 MsgText)也属于 Widget
该类的 handle() 方法通过 hasattr()/getattr() 进行动态分发:决定由谁来处理特定请求(事件)。若当前控件不支持该事件,则有两级回退机制

  1. 若存在父控件,则调用父控件handle()
  2. 若没有父控件但定义了 handle_default(),则调用默认处理。代码如下:
class Widget:
    def __init__(self, parent=None):
        self.parent = parent
    def handle(self, event):
        handler = f"handle_{event}"
        if hasattr(self, handler):
            method = getattr(self, handler)
            method(event)
        elif self.parent is not None:
            self.parent.handle(event)
        elif hasattr(self, "handle_default"):
            self.handle_default(event)

此处你也能理解为何在 UML 中 WidgetEvent 只是关联关系:Widget 知道 Event 的存在,但不持有严格引用;事件只需作为 handle()参数传入即可。

MainWindowSendDialogMsgText 都是具有不同行为的控件。它们不一定能处理相同事件,即便能处理,行为也可能不同。
MainWindow 只处理 closedefault 事件:

class MainWindow(Widget):
    def handle_close(self, event):
        print(f"MainWindow: {event}")
    def handle_default(self, event):
        print(f"MainWindow Default: {event}")

SendDialog 只处理 paint 事件:

class SendDialog(Widget):
    def handle_paint(self, event):
        print(f"SendDialog: {event}")

MsgText 只处理 down 事件:

class MsgText(Widget):
    def handle_down(self, event):
        print(f"MsgText: {event}")

main() 展示了如何创建若干控件与事件,并观察控件对事件的反应。我们将所有事件都发送给每个控件。注意父子关系:sdSendDialog 实例)的父控件是 mwMainWindow 实例);msgMsgText 实例)的父控件是 sd,并非必须直接隶属于 MainWindow

def main():
    mw = MainWindow()
    sd = SendDialog(mw)
    msg = MsgText(sd)
    for e in ("down", "paint", "unhandled", "close"):
        evt = Event(e)
        print(f"Sending event -{evt}- to MainWindow")
        mw.handle(evt)
        print(f"Sending event -{evt}- to SendDialog")
        sd.handle(evt)
        print(f"Sending event -{evt}- to MsgText")
        msg.handle(evt)

实现要点回顾(见 ch05/chain.py):

  • 定义 Event,随后定义 Widget
  • 定义三个特化控件:MainWindowSendDialogMsgText
  • 添加 main() 并按惯例在文件末尾保证可调用。

运行 python ch05/chain.py,输出示例:

Sending event -down- to MainWindow
MainWindow Default: down
Sending event -down- to SendDialog
MainWindow Default: down
Sending event -down- to MsgText
MsgText: down
Sending event -paint- to MainWindow
MainWindow Default: paint
Sending event -paint- to SendDialog
SendDialog: paint
Sending event -paint- to MsgText
SendDialog: paint
Sending event -unhandled- to MainWindow
MainWindow Default: unhandled
Sending event -unhandled- to SendDialog
MainWindow Default: unhandled
Sending event -unhandled- to MsgText
MainWindow Default: unhandled
Sending event -close- to MainWindow
MainWindow: close
Sending event -close- to SendDialog
MainWindow: close
Sending event -close- to MsgText
MainWindow: close

命令(Command)模式

当今大多数应用都有**撤销(undo)**功能。很难想象,但在很长一段时间里,软件中并没有撤销:撤销诞生于 1974 年,而至今仍被广泛使用的 Fortran 与 Lisp 分别诞生于 19571958 年!那段岁月里当应用用户可不轻松——一旦出错,几乎没有简便的方法修复。

历史就说到这儿。我们更关心如何在自己的应用里实现撤销。既然你已看到本章标题,你大概知道实现撤销推荐使用的设计模式是:命令模式

命令模式帮助我们把一个操作(撤销、重做、复制、粘贴等)封装为对象。这意味着我们创建一个类,内部包含实现该操作所需的全部逻辑与方法。这样做的好处包括:

  • 命令不必立即执行,而是可以在需要时再执行。
  • 调用者(invoker)与执行者解耦:调用者无需了解命令的实现细节。
  • 若有需要,可以把多个命令分组,让调用者按顺序执行它们(例如实现多级撤销)。

现实世界示例

去餐馆点餐时,我们把订单交给服务员。服务员写在点菜单(小票)上的内容就是命令的一个例子。写好后,服务员把小票放到队列里,由后厨按序执行。每张小票是独立的,并且可以包含多条命令(例如每道菜一条)。

在软件领域,也有不少例子,例如:

  • PyQt(Qt 的 Python 绑定)中有 QAction 类,把一个动作建模为命令。每个动作还可附带说明、提示、快捷键等可选信息。
  • Git Cola(用 Python 编写的 Git GUI)使用命令模式来修改模型、修补提交、应用不同选择、检出分支,等等。

适用场景

许多开发者把撤销当作命令模式的唯一用例。诚然,撤销是命令模式的“杀手级”特性,但命令模式的能力远不止于此:

  • GUI 按钮与菜单项:如前述 PyQt 的例子,使用命令模式来实现按钮/菜单项的动作。
  • 其他操作:除撤销外,还可用于剪切、复制、粘贴、重做、大小写转换等。
  • 事务行为与日志:用于持久记录变更,支持操作系统从崩溃中恢复、关系数据库的事务、文件系统快照、安装向导的回滚等。
  • 宏(Macros) :这里指可录制的动作序列,可随时回放。Emacs、Vim 等编辑器都支持宏。

实现命令模式

我们用命令模式封装几个基础的文件工具:

  • 创建文件,并可选地写入文本
  • 读取文件内容
  • 重命名文件

这些工具不必从零实现——Python 的 os 模块已经提供了很好的能力。我们要做的是在其之上加一层抽象,把它们当作命令对待,从而获得命令带来的种种好处。

每个命令包含两部分:

  • 初始化:由 __init__() 完成,收集命令执行所需的信息(如文件路径、要写入的内容等)。
  • 执行:由 execute() 完成;当我们希望运行命令时调用它(不一定在初始化之后立刻执行)。

先从重命名工具开始,用 RenameFile 类实现。构造时传入源与目标路径;execute() 中调用 os.rename() 真正执行重命名。为支持撤销,我们实现 undo(),再次用 os.rename() 把文件名改回去。示例中也使用了 logging 改善输出。

开头的导入与 RenameFile 如下:

import logging
import os
logging.basicConfig(level=logging.DEBUG)
class RenameFile:
    def __init__(self, src, dest):
        self.src = src
        self.dest = dest
    def execute(self):
        logging.info(
            f"[renaming '{self.src}' to '{self.dest}']"
        )
        os.rename(self.src, self.dest)
    def undo(self):
        logging.info(
            f"[renaming '{self.dest}' back to '{self.src}']"
        )
        os.rename(self.dest, self.src)

接着添加用于创建文件的命令 CreateFile。其构造接受文件路径 path 与写入内容 txt。若未提供内容,示例默认写入 "hello world\n"(现实里更合理的默认是空文件,这里为示例效果选择写入一段文本)。execute() 使用 open(..., "w")write() 写入。

创建文件的撤销操作就是删除该文件,于是实现 undo(),调用 os.remove()

class CreateFile:
    def __init__(self, path, txt="hello world\n"):
        self.path = path
        self.txt = txt
    def execute(self):
        logging.info(f"[creating file '{self.path}']")
        with open(
            self.path, "w", encoding="utf-8"
        ) as out_file:
            out_file.write(self.txt)
    def undo(self):
        logging.info(f"deleting file {self.path}")
        os.remove(self.path)

最后实现读取文件的命令 ReadFile:其 execute() 用只读方式打开并打印内容:

class ReadFile:
    def __init__(self, path):
        self.path = path
    def execute(self):
        logging.info(f"[reading file '{self.path}']")
        with open(
            self.path, "r", encoding="utf-8"
        ) as in_file:
            print(in_file.read(), end="")

main() 使用上述命令。orig_namenew_name 是创建并重命名的原/新文件名。我们把要稍后执行的命令按顺序放进 commands 列表:

def main():
    orig_name, new_name = "file1", "file2"
    commands = (
        CreateFile(orig_name),
        ReadFile(orig_name),
        RenameFile(orig_name, new_name),
    )
for c in commands:
    c.execute()

随后询问用户是否需要撤销已执行的命令。若选择撤销,就对 commands 逆序调用 undo()。注意并非所有命令都支持撤销,因此用异常处理捕获 undo() 缺失时的 AttributeError 并记录日志:

    answer = input("reverse the executed commands? [y/n] ")
    if answer not in "yY":
        print(f"the result is {new_name}")
        exit()
    for c in reversed(commands):
        try:
            c.undo()
        except AttributeError as e:
            logging.error(str(e))

实现回顾(见 ch05/command.py):

  • 导入 loggingos
  • 进行常规日志配置;
  • 定义 RenameFile
  • 定义 CreateFile
  • 定义 ReadFile
  • 编写并调用 main() 测试设计。

当选择撤销时,运行 python ch05/command.py 的输出示例:

INFO:root:[creating file 'file1']
INFO:root:[reading file 'file1']
hello world
INFO:root:[renaming 'file1' to 'file2']
reverse the executed commands? [y/n] y
INFO:root:[renaming 'file2' back to 'file1']
ERROR:root:'ReadFile' object has no attribute 'undo'
INFO:root:deleting file file1

当选择不撤销时,输出如下:

INFO:root:[creating file 'file1']
INFO:root:[reading file 'file1']
hello world
INFO:root:[renaming 'file1' to 'file2']
reverse the executed commands? [y/n] n
the result is file2

这些输出符合预期。需要说明的是,第一种情况下出现的 ERROR 在此语境下是正常的(因为 ReadFile 没有 undo())。

观察者(Observer)模式

观察者模式描述了发布–订阅关系:单个对象(发布者,也称 主题/可观察者:subject/observable)与一个或多个对象(订阅者,也称 观察者:observers)之间的协作。主题在自身状态变化时通知订阅者,通常通过调用订阅者的某个方法来完成。

该模式背后的思想与关注点分离原则一致:增强发布者与订阅者之间的解耦,并便于在运行时动态添加/移除订阅者。

现实世界示例

  • 拍卖的动态与观察者模式十分相似。每位竞拍者手持号牌,需要出价时举起号牌。拍卖师充当主题:在有人举牌时更新出价并广播新价格给所有竞拍者(订阅者)。

  • 在软件领域,至少有两个例子:

    • Kivy(用于开发 UI 的 Python 框架)有一个 Properties 模块,实现了观察者模式。你可以声明当某个属性值变化时应该发生什么。
    • RabbitMQ 提供 AMQP 消息代理的实现。Python 应用可以与 RabbitMQ 交互,订阅消息并将其发布到队列,本质上就是观察者模式。

适用场景

当我们希望在某个对象(主题/发布者/可观察者)发生变化时,通知/更新一个或多个对象(观察者/订阅者),通常会使用观察者模式。观察者的数量与身份可能变化,并且可以动态调整

常见用例包括:

  • 资讯订阅:如 RSS/Atom 等。你关注一个 feed,每次它更新时都会收到通知。
  • 社交应用:与你互相关注的人更新了内容,你会收到通知。
  • 事件驱动系统:有若干监听器监听特定事件;当事件发生(如按键、鼠标移动等),对应监听器被触发。此时事件扮演发布者监听器扮演观察者;关键在于多个观察者可以附着在同一个发布者上。

实现观察者模式

天气监测系统为例:一座气象站(收集温度、湿度、气压等数据)需要让不同设备/应用在数据变化时实时接收更新

我们按以下要素应用观察者模式:

  • 主题(气象站)WeatherStation 维护一个对天气更新感兴趣的观察者列表
  • 观察者(设备与应用) :实现多个观察者类,代表手机、平板、天气应用,或本地商店的显示屏等;它们向气象站订阅更新。
  • 注册与通知:气象站提供注册/注销方法。当天气数据变化(如新温度值)时,气象站通知所有已注册观察者
  • 更新机制:每个观察者实现 update(),当被通知时执行相应更新逻辑(例如手机应用刷新界面,本地显示屏更新看板)。

下面开始实现。

首先定义 Observer 接口,规定观察者必须实现 update 方法;当主题状态变化时,观察者更新自身

class Observer:
    def update(self, temperature, humidity, pressure):
        pass

接着定义 WeatherStation(主题) 。它维护观察者列表并提供添加/移除观察者的方法。set_weather_data 用于模拟天气数据变化;一旦变化就逐一调用观察者的 update

class WeatherStation:
    def __init__(self):
        self.observers = []
    def add_observer(self, observer):
        self.observers.append(observer)
    def remove_observer(self, observer):
        self.observers.remove(observer)
    def set_weather_data(self, temperature, humidity, pressure):
        for observer in self.observers:
            observer.update(temperature, humidity, pressure)

定义一个观察者 DisplayDevice,其 update 方法在被调用时打印天气信息:

class DisplayDevice(Observer):
    def __init__(self, name):
        self.name = name
    def update(self, temperature, humidity, pressure):
        print(f"{self.name} Display")
        print(
            f" - Temperature: {temperature}°C, Humidity: {humidity}%, Pressure: {pressure}hPa"
        )

再定义另一个观察者 WeatherApp,以不同格式打印更新:

class WeatherApp(Observer):
    def __init__(self, name):
        self.name = name
    def update(self, temperature, humidity, pressure):
        print(f"{self.name} App - Weather Update")
        print(
            f" - Temperature: {temperature}°C, Humidity: {humidity}%, Pressure: {pressure}hPa"
        )

main() 中我们将:

  • 创建 WeatherStation(主题实例);
  • 创建 DisplayDeviceWeatherApp(不同观察者);
  • 通过 add_observer 将它们注册到气象站;
  • 通过调用 set_weather_data 模拟天气数据变化,从而触发对所有观察者的通知
def main():
    # Create the WeatherStation
    weather_station = WeatherStation()
    # Create and register observers
    display1 = DisplayDevice("Living Room")
    display2 = DisplayDevice("Bedroom")
    app1 = WeatherApp("Mobile App")
    weather_station.add_observer(display1)
    weather_station.add_observer(display2)
    weather_station.add_observer(app1)
    # Simulate weather data changes
    weather_station.set_weather_data(25.5, 60, 1013.2)
    weather_station.set_weather_data(26.0, 58, 1012.8)

实现要点回顾(见 ch05/observer.py):

  • 定义 Observer 接口;
  • 定义主题类 WeatherStation
  • 定义两个观察者类:DisplayDeviceWeatherApp
  • main() 中测试设计。

运行 python ch05/observer.py,示例输出如下:

Living Room Display
- Temperature: 25.5°C, Humidity: 60%, Pressure: 1013.2hPa
Bedroom Display
- Temperature: 25.5°C, Humidity: 60%, Pressure: 1013.2hPa
Mobile App App - Weather Update
- Temperature: 25.5°C, Humidity: 60%, Pressure: 1013.2hPa
Living Room Display
- Temperature: 26.0°C, Humidity: 58%, Pressure: 1012.8hPa
Bedroom Display
- Temperature: 26.0°C, Humidity: 58%, Pressure: 1012.8hPa
Mobile App App - Weather Update
- Temperature: 26.0°C, Humidity: 58%, Pressure: 1012.8hPa

如上所示,主题在状态变化时通知其观察者;观察者与主题松耦合,可动态添加/移除,从而为系统提供灵活性与解耦。

练习:尝试在调用 remove_observer() 后再次模拟天气变化,此时只有剩余已注册的观察者会收到更新。可在 main() 末尾加入这两行测试代码:

    weather_station.remove_observer(display2)
    weather_station.set_weather_data(27.2, 55, 1012.5)

接下来,我们将讨论状态(State)模式

状态(State)模式

上一节我们介绍了观察者模式,它用于在某个对象状态变化时通知其他对象。继续探索 GoF 提出的那些模式吧。

OOP 的核心之一是维护彼此交互对象的状态。在许多问题中,建模状态转换的一个非常实用的工具叫作有限状态机(finite-state machine,简称状态机)。

什么是状态机?状态机是一种抽象机器,包含两个关键组件:状态转换状态表示系统当前(活跃)的状况。比如一台收音机,可能处于 FMAM 调谐状态,或处于在不同 FM/AM 电台之间切换的状态。转换是从一个状态切换到另一个状态,由某个触发事件或条件引发,通常在转换发生之前或之后会执行一个或一组动作。假设收音机当前调到 107 FM,听众按下按钮切到 107.5 FM,这个动作就是一次转换。

状态机的一个优点是能用图(状态图)来表示:每个状态是一个节点,每条转换是连接两个节点的

状态机可用于解决多种非计算计算问题。非计算的例子包括售货机、电梯、交通信号灯、密码锁、停车计时器、自助加油机等。计算领域的例子包括游戏编程、其它软件编程类别、硬件设计、协议设计、编程语言解析等。

现在我们了解了状态机是什么!它与状态模式有什么关系?事实上,状态模式就是将状态机应用到特定软件工程问题的一种方式(参见 GoF-95,第 342 页;以及 Python 3 Patterns, Recipes and Idioms by Bruce Eckel & Friends,第 151 页)。

现实世界示例

零食售货机就是状态模式的日常例子。售货机有多个状态,会根据你投币的金额作出不同反应。根据你的选择与投币金额,机器可能:

  • 拒绝选择(所选商品缺货);
  • 拒绝选择(投币金额不足);
  • 正好金额:出货且不找零
  • 金额超出:出货并找零

当然还有更多可能状态,但你已经明白要点了。其他例子还包括:

  • 交通信号灯
  • 电子游戏的关卡/角色状态

在软件中,状态模式也很常见。Python 生态提供了多个实现状态机的包/模块。实现部分我们将使用其中一个。

适用场景

所有可用状态机解决的问题都是状态模式的良好用例。我们已经见过的一个例子是操作系统/嵌入式系统进程模型

编译器实现也是好例子:词法/语法分析可以用状态来构建抽象语法树(AST)。

事件驱动系统同样适用:系统从一个状态过渡到另一个状态时触发事件/消息。许多电脑游戏使用这种技术:例如当主角靠近时,怪物可能从警戒状态切到攻击状态。

引用 Thomas Jaeger 在 The State Design Pattern vs. State Machinethomasjaeger.wordpress.com/2012/12/13/…)中的一句话:

状态设计模式允许在一个上下文中对无限多的状态进行完全封装,从而带来易维护性与灵活性

实现状态模式

我们来写段代码,基于本章前面提到的状态图,演示如何创建一个状态机。这个状态机涵盖进程的不同状态及其之间的转换。

状态模式通常这样实现:定义一个父类 State,其中包含所有状态的公共功能;再派生出若干具体状态类,每个只包含该状态所需的专用功能。状态模式关注的是实现一个状态机——核心即状态与它们之间的转换,至于如何实现这些部分并不限定。

为避免重复造轮子,我们使用 Python 的 state_machine 模块:它既能帮助创建状态机,又颇为 Pythonic。该模块简单易用,我们会边写代码边说明。

首先定义 Process 类。每个创建的进程都有自己的状态机。用 state_machine 创建状态机的第一步是添加装饰器 @acts_as_state_machine。然后定义状态机的状态,与状态图一一对应;不同的是,需要指明初始状态,通过将 initial=True 设置在对应的 State 上:

@acts_as_state_machine
class Process:
    created = State(initial=True)
    waiting = State()
    running = State()
    terminated = State()
    blocked = State()
    swapped_out_waiting = State()
    swapped_out_blocked = State()

接下来定义转换。在 state_machine 模块中,转换由 Event 类的实例表示。我们通过 from_statesto_state 指定可能的转换:

wait = Event(
    from_states=(
        created,
        running,
        blocked,
        swapped_out_waiting,
    ),
    to_state=waiting,
)
run = Event(
    from_states=waiting, to_state=running
)
terminate = Event(
    from_states=running, to_state=terminated
)
block = Event(
    from_states=(
        running,
        swapped_out_blocked,
    ),
    to_state=blocked,
)
swap_wait = Event(
    from_states=waiting,
    to_state=swapped_out_waiting,
)
swap_block = Event(
    from_states=blocked,
    to_state=swapped_out_blocked,
)

注意:from_states 既可以是单个状态,也可以是多个状态的元组

每个进程都有一个名字。严格来说,一个“有用”的进程还应包含更多信息(ID、优先级、状态等),但这里保持简洁以聚焦模式本身:

def __init__(self, name):
    self.name = name

若转换发生时什么都不做就没意义了。state_machine 提供 @before@after 装饰器,分别在转换前/后执行动作。实际中你可以更新系统中的对象、发送邮件/通知等。这里我们仅打印进程状态变化信息:

@after("wait")
def wait_info(self):
    print(f"{self.name} entered waiting mode")

@after("run")
def run_info(self):
    print(f"{self.name} is running")

@before("terminate")
def terminate_info(self):
    print(f"{self.name} terminated")

@after("block")
def block_info(self):
    print(f"{self.name} is blocked")

@after("swap_wait")
def swap_wait_info(self):
    print(
        f"{self.name} is swapped out and waiting"
    )

@after("swap_block")
def swap_block_info(self):
    print(
        f"{self.name} is swapped out and blocked"
    )

接着需要一个 transition() 函数,接受三个参数:

  • procProcess 实例
  • eventEvent 实例(waitrunterminate 等)
  • event_name:该事件的名称

当执行转换失败时,打印事件名以便定位。代码如下:

def transition(proc, event, event_name):
    try:
        event()
    except InvalidStateTransition:
        msg = (
            f"Transition of {proc.name} from {proc.current_state} "
            f"to {event_name} failed"
        )
        print(msg)

state_info() 用于显示进程当前(活跃)状态的基本信息:

def state_info(proc):
    print(
        f"state of {proc.name}: {proc.current_state}"
    )

main() 的开头,定义一些字符串常量,作为 event_name 传入:

def main():
    RUNNING = "running"
    WAITING = "waiting"
    BLOCKED = "blocked"
    TERMINATED = "terminated"

然后创建两个 Process 实例并显示它们的初始状态:

p1, p2 = Process("process1"), Process(
    "process2"
)
[state_info(p) for p in (p1, p2)]

函数的其余部分尝试不同的转换。请记住我们前面提到的状态图:只允许图中定义的转换。例如,可以从 running 切到 blocked,但不允许blocked 切回 running

print()
transition(p1, p1.wait, WAITING)
transition(p2, p2.terminate, TERMINATED)
[state_info(p) for p in (p1, p2)]
print()
transition(p1, p1.run, RUNNING)
transition(p2, p2.wait, WAITING)
[state_info(p) for p in (p1, p2)]
print()
transition(p2, p2.run, RUNNING)
[state_info(p) for p in (p1, p2)]
print()
[    transition(p, p.block, BLOCKED)    for p in (p1, p2)]
[state_info(p) for p in (p1, p2)]
print()
[    transition(p, p.terminate, TERMINATED)    for p in (p1, p2)]
[state_info(p) for p in (p1, p2)]

实现回顾(完整示例见 ch05/state.py):

  • state_machine 导入所需内容;
  • 定义带简单属性的 Process 类;
  • 添加 Process 的初始化方法;
  • Process 内定义状态与事件(转换);
  • 定义 transition() 函数;
  • 定义 state_info() 函数;
  • 添加程序的 main()

运行 python ch05/state.py,示例输出:

state of process1: created
state of process2: created
process1 entered waiting mode
Transition of process2 from created to terminated failed
state of process1: waiting
state of process2: created
process1 is running
process2 entered waiting mode
state of process1: running
state of process2: waiting
process2 is running
state of process1: running
state of process2: running
process1 is blocked
process2 is blocked
state of process1: blocked
state of process2: blocked
Transition of process1 from blocked to terminated failed
Transition of process2 from blocked to terminated failed
state of process1: blocked
state of process2: blocked

如你所见,非法转换(如 created → terminatedblocked → terminated)均被优雅地拒绝。当请求非法转换时,我们不希望程序崩溃,except 块已经妥善处理。

使用像 state_machine 这样的优秀库可以消除大量条件分支:无需为每一种状态转换写又长又易错的 if…else

想更好地体会状态模式与状态机,强烈建议你自己实现一个例子:可以是简单游戏(用状态机管理主角与敌人的状态)、电梯解析器,或任何可用状态机建模的系统。

解释器(Interpreter)模式

我们常常需要创建领域特定语言(DSL) 。DSL 是面向特定领域、表达力受限的计算机语言,常见于作战模拟、计费、可视化、配置与通信协议等。DSL 分为内部 DSL外部 DSL

内部 DSL构建在某个宿主编程语言之上。比如用 Python 编写一个解线性方程的“小语言”。其优点是无需自建/编译/解析语法——宿主语言已提供这些能力;缺点是受制于宿主语言特性:若宿主语言缺乏足够的表达力与流畅语法,打造简洁、优雅的内部 DSL 就会很难。

外部 DSL不依赖宿主语言;DSL 的设计者可以完全决定其语法/语法规则等,并且要负责为其编写解析器与编译器

解释器模式只与内部 DSL相关。它的目标是在宿主语言(此处为 Python)提供的特性之上,创建一个简单而实用的语言。注意,解释器模式不处理解析:它假定我们已经拥有了“方便使用”的已解析数据(如 AST 或其它合适的数据结构)【GoF-95,p.276】。

现实世界示例

  • 音乐家是解释器模式的一个例子。乐谱以图形化方式表示音高与时值;音乐家可据谱准确地“解释/演奏”声音。就某种意义上说,乐谱是音乐的语言,而音乐家是该语言的解释器

  • 软件中的例子:

    • C++ 世界里,boost::spirit 常被视为用于实现解析器的内部 DSL
    • Python 中的 PyT 是用于生成 XHTML/HTML 的内部 DSL,主打性能(声称与 Jinja2 速度相当)。当然,我们不应默认 PyT 必然使用了解释器模式;但作为内部 DSL,解释器模式很可能适用。

适用场景

当我们希望给领域专家/高级用户提供一门简单语言来解决问题时,可采用解释器模式。需要强调:解释器模式只适合实现简单语言。如果语言需求已经接近外部 DSL,应使用更专业的语言工具(如 Yacc/Lex、Bison、ANTLR 等)。

我们的目标是为非程序员(或不需要掌握高级 Python 的用户)提供合适的抽象,让他们高效完成任务。理想情况下,用户不必懂高级 Python;懂一点 Python 会有帮助,但不应成为硬性要求。此外,DSL 的性能通常不是核心;关键是隐藏宿主语言的古怪之处,提供更可读的语法(当然,Python 本就比许多语言更易读)。

实现解释器模式

下面用解释器模式创建一个智能家居的内部 DSL。这很契合日益流行的 IoT 场景。用户用一种非常简单的事件表示法控制家居设备:

命令 -> 接收者 -> 参数

其中“参数”可选。

不带参数的事件示例:

open -> gate

带参数的事件示例:

increase -> boiler temperature -> 3 degrees

这里用符号 -> 来分隔事件的各个部分。实现内部 DSL 的方式很多:正则/字符串处理运算符重载 + 元编程,或借助库/工具完成繁琐部分。尽管从定义上解释器模式不负责解析,但为了使示例更实用,我们也一并展示解析。这部分我们使用 pyparsing(可参考 Paul McGuire 的小册 Getting Started with Pyparsing)。

在写代码前,先用 BNF 记法为该语言描述一个简单语法:

event ::= command token receiver token arguments
command ::= word+
word ::= a collection of one or more alphanumeric characters
token ::= ->
receiver ::= word+
arguments ::= word+

含义:事件形如 command -> receiver -> arguments;其中 command / receiver / arguments 都是由一个或多个字母数字单词组成。包含数字是为了能表达参数(比如 3 degrees)。

接着把语法用代码表达(自底向上定义):

from pyparsing import Word, OneOrMore, Group, Suppress, Optional, alphanums

word = Word(alphanums)
command = Group(OneOrMore(word))
token = Suppress("->")          # 跳过 '->',不出现在解析结果中
device = Group(OneOrMore(word))
argument = Group(OneOrMore(word))
event = command + token + device + Optional(token + argument)

注:上面使用 Suppress("->") 表示在解析结果里忽略该分隔符。

接着给出一个最小可运行的设备类 Boiler。锅炉默认温度 83℃,并提供升/降温方法:

class Boiler:
    def __init__(self):
        self.temperature = 83  # in celsius
    def __str__(self):
        return f"boiler temperature: {self.temperature}"
    def increase_temperature(self, amount):
        print(f"increasing the boiler's temperature by {amount} degrees")
        self.temperature += amount
    def decrease_temperature(self, amount):
        print(f"decreasing the boiler's temperature by {amount} degrees")
        self.temperature -= amount

将语法与设备拼在一起,并创建一个实例:

boiler = Boiler()

pyparsingparseString() 方法会返回 ParseResults(可当作嵌套列表)。例如:

print(event.parseString("increase -> boiler temperature -> 3 degrees"))
# 结果类似:
# [['increase'], ['boiler', 'temperature'], ['3', 'degrees']]

因此,第一子列表是命令(increase),第二是接收者(boiler temperature),第三是参数(3 degrees)。我们可以拆包解析结果,进而匹配相应的方法执行:

test = "increase -> boiler temperature -> 3 degrees"
cmd, dev, arg = event.parseString(test)
cmd_str = " ".join(cmd)
dev_str = " ".join(dev)
if "increase" in cmd_str and "boiler" in dev_str:
    boiler.increase_temperature(int(arg[0]))
print(boiler)

运行(python ch05/interpreter/boiler.py)输出:

increasing the boiler's temperature by 3 degrees
boiler temperature: 86

完整实现(见 ch05/interpreter/interpreter.py)与上面描述大同小异,只是扩展以支持更多事件与设备。步骤概述:

  1. pyparsing 导入所需内容;

  2. 定义设备类:GateAirconditionHeatingBoiler(已展示)与 Fridge

  3. main() 中:

    • 准备测试用的 testsopen_actionsclose_actions
    • 执行各项测试。

运行 python ch05/interpreter/interpreter.py 的示例输出:

opening the gate
closing the garage
turning on the air condition
turning off the heating
increasing the boiler's temperature by 5 degrees
decreasing the fridge's temperature by 2 degrees

如果你想继续拓展这个示例,建议先把它做成交互式:目前事件都硬编码在 tests 元组里,用户应该能通过交互式提示输入事件。别忘了留意 pyparsing空格/制表符/异常输入的敏感性。例如,用户输入 turn off -> heating 37 会发生什么?(提示:考虑健壮的错误处理与更严格的语法约束。)

策略(Strategy)模式

对于同一个问题,往往存在多种解法。以排序为例:要把列表元素按特定次序排列,我们可以选择多种算法。通常不存在在所有场景都绝对最优的单一算法。

选择排序算法要结合具体情境,常见考量包括:

  • 输入规模(待排序元素个数):大多数算法在小规模数据上都表现不错,但只有少数在大规模数据上仍然高效。
  • 时间复杂度(最好/平均/最坏) :粗略表示算法完成所需时间(忽略常数与低阶项)。这是常用选择标准,但并不总是充足依据。
  • 空间复杂度:粗略表示算法执行所需的物理内存。在大数据嵌入式系统(内存受限)中尤为重要。
  • 稳定性:若算法能在排序后保持相等元素的相对次序,则称其为稳定
  • 代码复杂度:当两种算法在时间/空间复杂度与稳定性上相当时,更易实现与维护者更具优势。

还有其他因素可能影响选择。关键在于:是否必须“一招鲜吃遍天” ?答案当然是否定的。更实际的做法是准备多种算法,并根据准则为具体情境挑选最合适的一种——这正是策略模式的要义。

策略模式提倡用多种算法来解决同一问题。其“杀手锏”是:可在运行时透明切换算法(客户端无感知)。因此,如果你知道某算法在小输入更好、另一算法在大输入更佳,就可以在运行时根据数据选择策略。

现实世界示例

去机场赶飞机的出行方式就很“策略”:

  • 想省钱且出发早:公交/地铁
  • 不介意停车费且有车:自驾
  • 没车又赶时间:打车

这其中权衡了成本/时间/便利等因素。

在软件里,Python 的 sorted()list.sort() 就体现了策略模式:二者都接收命名参数 key,本质上就是一个实现排序策略的函数(参见 Python 3 Patterns, Recipes, and Idioms,p.202)。

适用场景

策略是非常通用的设计模式:当我们希望动态且透明地应用不同算法时,就该考虑它。这里的“不同算法”,也可以理解为同一问题的不同实现——结果应一致,但性能实现复杂度可能不同(如顺序查找 vs 二分查找)。

除了上面提到的排序,策略模式还常用于不同格式化表示的生成,既可以为跨平台(例如换行差异)服务,也可以在运行时动态改变数据的呈现形式。

实现策略模式

策略模式的实现并不复杂。在函数不是一等公民的语言里,常用一个类对应一个策略。而在 Python 里,函数就是对象(可被变量引用与操作),使策略模式实现更为简洁。

设计算法:判断一个字符串中所有字符是否唯一。例如:

  • 输入 dream,返回 True(无重复字符);
  • 输入 pizza,返回 False(字母 z 出现两次);
  • 输入 1r2a3ae,返回 False(字母 a 出现两次)。

注意:重复字符不必相邻,字符串也不必是有效单词。

我们先想到一种实现:先排序字符串,再成对比较字符。

邻接对生成器

def pairs(seq):
    n = len(seq)
    for i in range(n):
        yield seq[i], seq[(i + 1) % n]

策略一:排序 + 成对比较

为演示策略差异,假设该算法在长度 ≤ 5 时表现尚可,更长字符串时明显变慢(用 sleep 模拟):

import time

SLOW = 3   # seconds
LIMIT = 5  # characters
WARNING = "too bad, you picked the slow algorithm :("

def allUniqueSort(s):
    if len(s) > LIMIT:
        print(WARNING)
        time.sleep(SLOW)
    srtStr = sorted(s)
    for c1, c2 in pairs(srtStr):
        if c1 == c2:
            return False
    return True

策略二:集合判重

再想一个无需排序的实现:用 set 判重。为对比,假设它在短字符串上反而慢一点(同样用 sleep 模拟):

def allUniqueSet(s):
    if len(s) < LIMIT:
        print(WARNING)
        time.sleep(SLOW)
    return len(set(s)) == len(s)

统一调度入口

allUnique(s, strategy) 接收输入字符串与一个策略函数allUniqueSortallUniqueSet),执行之并返回结果;main() 里让用户输入单词并选择策略,同时做基本错误处理并可优雅退出

def allUnique(s, strategy):
    return strategy(s)

def main():
    WORD_IN_DESC = "Insert word (type quit to exit)> "
    STRAT_IN_DESC = "Choose strategy: [1] Use a set, [2] Sort and pair> "
    while True:
        word = None
        while not word:
            word = input(WORD_IN_DESC)
            if word == "quit":
                print("bye")
                return
            strategy_picked = None
            strategies = {"1": allUniqueSet, "2": allUniqueSort}
            while strategy_picked not in strategies.keys():
                strategy_picked = input(STRAT_IN_DESC)
                try:
                    strategy = strategies[strategy_picked]
                    result = allUnique(word, strategy)
                    print(f"allUnique({word}): {result}")
                except KeyError:
                    print(f"Incorrect option: {strategy_picked}")

实现回顾(ch05/strategy.py

  • 导入 time
  • 定义 pairs()
  • 定义常量 SLOWLIMITWARNING
  • 定义算法一 allUniqueSort()
  • 定义算法二 allUniqueSet()
  • 定义调度函数 allUnique()
  • 编写 main()

示例输出

Insert word (type quit to exit)> balloon
Choose strategy: [1] Use a set, [2] Sort and pair> 1
allUnique(balloon): False

Insert word (type quit to exit)> balloon
Choose strategy: [1] Use a set, [2] Sort and pair> 2
too bad, you picked the slow algorithm :(
allUnique(balloon): False

Insert word (type quit to exit)> bye
Choose strategy: [1] Use a set, [2] Sort and pair> 1
too bad, you picked the slow algorithm :(
allUnique(bye): True

Insert word (type quit to exit)> bye
Choose strategy: [1] Use a set, [2] Sort and pair> 2
allUnique(bye): True
Insert word (type quit to exit)>

解释:

  • 单词 balloon 长度 > 5 且存在重复字符。两种算法都返回 False,但 allUniqueSort() 被模拟为更慢,并给出警告。
  • 单词 bye 长度 < 5 且字符唯一。两种算法都返回 True,但此时 allUniqueSet() 被模拟为更慢,并给出警告。

改进建议:正常情况下,无需用户选择策略;策略模式的意义在于透明选择最合适的算法。请修改代码,使程序自动挑选更快的实现(例如以 LIMIT 为阈值:短字符串用 allUniqueSort(),长字符串用 allUniqueSet(),并移除 sleep 与提示)。

面向开发者的 API 设计:若还要为其他开发者提供 API,可将两种函数封装到一个类(如 AllUnique)中,暴露单一方法 test()。该方法内部:

  1. 依据输入长度(或统计学特征)选择策略
  2. 调用相应实现并返回布尔结果
  3. 可选:记录统计/日志(输入规模、所选策略、耗时),便于后续优化。

备忘录(Memento)模式

在许多场景下,我们需要一种方式便捷地为对象的内部状态拍快照,以便在需要时用这份快照恢复对象。备忘录模式正是为此而生。

备忘录模式包含三个关键角色:

  • Memento(备忘录) :一个简单对象,负责保存/恢复状态的数据载体。
  • Originator(发起人) :能够从/向备忘录获取/设置状态的对象。
  • Caretaker(管理者) :负责存储与取回先前创建的所有备忘录实例的对象。

备忘录模式与命令模式有不少相似之处。

现实世界示例

生活中有很多类似备忘录的情形。

  • 例如权威词典(英语、法语等):语言随时间演化,词典会不断修订(增补新词、淘汰旧词)。我们有时需要回溯旧版本来了解某一时期语言的用法,或在信息散佚后通过档案找回历史记录。这一思路也可拓展到书籍、报纸等文献的版本追溯。
  • 软件中的例子:Zopewww.zope.org)集成的对象数据库ZODB 提供了网站管理的 Through-The-Web 界面,支持撤销(undo) 。ZODB 是 Python 的对象数据库,被 PyramidPlone 等技术栈广泛使用。

适用场景

  • 为用户提供撤销/重做能力时,常使用备忘录模式。
  • 另一个常见用法是带“确定/取消”的设置对话框:加载时保存对象初始状态;若用户点取消,则恢复到初始状态。

实现备忘录模式

下面用更贴近 Python 的简化方式来实现,不强求多个类。

我们将使用 Python 的 pickle 模块。其文档(docs.python.org/3/library/p…)说明:pickle能把复杂对象序列化为字节流,再从字节流反序列化回具有相同内部结构的对象。

警告
此处使用 pickle 仅为演示。通用场景下它并不安全,请谨慎使用。

定义一个 Quote 类,包含 textauthor。为了创建备忘录,增加 save_state() 方法,用 pickle.dumps() 转储对象状态(这里直接序列化 __dict__):

class Quote:
    def __init__(self, text, author):
        self.text = text
        self.author = author
    def save_state(self):
        current_state = pickle.dumps(self.__dict__)
        return current_state

稍后我们可恢复该状态:添加 restore_state(),使用 pickle.loads() 取回,再清空并更新 __dict__

    def restore_state(self, memento):
        previous_state = pickle.loads(memento)
        self.__dict__.clear()
        self.__dict__.update(previous_state)

再补一个 __str__ 便于打印:

    def __str__(self):
        return f"{self.text}\n- By {self.author}."

main() 中按常规测试:

def main():
    print("** Quote 1 **")
    q1 = Quote(
        "A room without books is like a body without a soul.",
        "Unknown author",
    )
    print(f"\nOriginal version:\n{q1}")
    q1_mem = q1.save_state()
    # 现在找到了作者姓名
    q1.author = "Marcus Tullius Cicero"
    print(f"\nWe found the author, and did an updated:\n{q1}")
    # 恢复到先前状态(撤销)
    q1.restore_state(q1_mem)
    print(f"\nWe had to restore the previous version:\n{q1}")

    print()
    print("** Quote 2 **")
    text = (
        "To be you in a world that is constantly \n"
        "trying to make you be something else is \n"
        "the greatest accomplishment."
    )
    q2 = Quote(
        text,
        "Ralph Waldo Emerson",
    )
    print(f"\nOriginal version:\n{q2}")
    _ = q2.save_state()
    # 修改文本
    q2.text = (
        "To be yourself in a world that is constantly \n"
        "trying to make you something else is the greatest \n"
        "accomplishment."
    )
    print(f"\nWe fixed the text:\n{q2}")
    q2_mem2 = q2.save_state()
    q2.text = (
        "To be yourself when the world is constantly \n"
        "trying to make you something else is the greatest \n"
        "accomplishment."
    )
    print(f"\nWe fixed the text again:\n{q2}")
    # 恢复到上一版本(撤销)
    q2.restore_state(q2_mem2)
    print(f"\nWe restored the 2nd version, the correct one:\n{q2}")

实现步骤回顾(ch05/memento.py):

  1. 导入 pickle
  2. 定义 Quote 类;
  3. main() 中测试实现。

运行 python ch05/memento.py,示例输出(节选):

** Quote 1 **
Original version:
A room without books is like a body without a soul.
- By Unknown author.
We found the author, and did an updated:
A room without books is like a body without a soul.
- By Marcus Tullius Cicero.
We had to restore the previous version:
A room without books is like a body without a soul.
- By Unknown author.
** Quote 2 **
...
We restored the 2nd version, the correct one:
To be yourself in a world that is constantly
trying to make you something else is the greatest
accomplishment.
- By Ralph Waldo Emerson.

如输出所示,程序按预期工作:我们可以为每个 Quote 对象恢复到先前状态

迭代器(Iterator)模式

在编程中,我们经常需要处理序列或对象集合,尤其是在算法实现和数据处理型程序(自动化脚本、API、数据驱动应用等)里。本章要介绍一个在处理对象集合时非常有用的模式:迭代器模式

注(根据维基百科的定义)
迭代器是一种设计模式:通过一个迭代器遍历容器并访问其中元素。迭代器模式将算法与容器解耦;但在某些情况下,算法必须依赖特定容器,因而无法完全解耦。

在 Python 语境中,迭代器模式被广泛使用——甚至成为语言特性:它太常用了,以至于语言设计者把它直接做进了语言。

适用场景

当你需要以下一种或多种行为时,考虑使用迭代器模式:

  • 方便地遍历一个集合
  • 随时获取集合中的下一个对象
  • 在遍历完成时自然停止

实现迭代器模式

在 Python 里,for 循环、列表推导等都内建了迭代器机制。迭代器就是可以被逐个遍历、一次返回一个元素的对象。

特殊场景下我们也可以自己实现,遵循迭代器协议:让迭代器对象实现两个特殊方法 __iter__()__next__()

一个对象若能从其中获取迭代器,就称它是可迭代(iterable) 。Python 中大多数内建容器(list、tuple、set、str 等)都是可迭代的。内建函数 iter()(内部会调用 __iter__())可从它们得到一个迭代器。

下面以一个足球队为例。我们用 FootballTeam 类来表示;由于它不是内建容器类型(如 list),要想迭代它,需要实现迭代器协议——否则内建的 iter()next() 无法工作。

先定义一个用于遍历球队成员的迭代器类 FootballTeamIterator。其 members 属性用于用我们的容器对象(FootballTeam 实例所持有的成员列表)来初始化迭代器;实现 __iter__() 返回自身,以及 __next__() 在每次调用时返回队伍中的下一个成员,直到遍历结束:

class FootballTeamIterator:
    def __init__(self, members):
        self.members = members
        self.index = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.index < len(self.members):
            val = self.members[self.index]
            self.index += 1
            return val
        else:
            raise StopIteration()

接着是容器类 FootballTeam 本身;为其添加 __iter__(),用来创建并返回相应的迭代器对象(这里用 FootballTeamIterator(self.members)):

class FootballTeam:
    def __init__(self, members):
        self.members = members
    def __iter__(self):
        return FootballTeamIterator(self.members)

写个小 main 来测试。创建 FootballTeam 实例后,对其调用 iter() 获得迭代器,并用 while 循环与 next() 手动遍历:

def main():
    members = [f"player{str(x)}" for x in range(1, 23)]
    members = members + ["coach1", "coach2", "coach3"]
    team = FootballTeam(members)
    team_it = iter(team)
    try:
        while True:
            print(next(team_it))
    except StopIteration:
        print("(End)")

示例步骤回顾(ch05/iterator.py):

  • 定义迭代器类
  • 定义容器类
  • 定义 main() 并调用。

运行 python ch05/iterator.py 的输出示例:

player1
player2
player3
player4
player5
...
player22
coach1
coach2
coach3
(End)

结果符合预期。可见当遍历到末尾时抛出了 StopIteration 异常,而我们捕获后打印了 (End)

模板(Template)模式

写出好代码的关键要素之一是避免重复。在 OOP 中,方法与函数是用来避免重复代码的重要工具。

还记得我们在讲策略模式时的 sorted() 例子吗?那个函数足够通用:它既能对列表、元组、具名元组等多种数据结构排序,也能接受任意键函数。这就是一个好函数的典范。

sorted() 这样的函数展示了理想情形;但我们并不能总是写出100% 通用的代码。

在处理真实世界中的算法时,我们常常会写出一些冗余的代码。模板模式要解决的正是这个问题:它聚焦于消除代码冗余。核心思想是:不改变算法整体结构的前提下,允许重定义算法中的某些步骤

现实世界示例

同一家公司里,员工的日常作息很接近模板模式:整体流程一致,但其中具体环节因人而异。

在软件中,Python 的 cmd 模块(用于构建行式命令解释器)采用了模板模式。具体来说,cmd.Cmd.cmdloop() 实现了一个“持续读取输入命令并分派到动作方法”的算法。循环前循环后、以及命令解析的部分始终不变(算法的不变部分);变化的是各个动作方法可变部分)。

适用场景

模板模式关注消除重复。当多个算法在结构上相似、且存在可复用的代码时,我们可以把算法的不变部分保留在模板方法/函数中,把各处不同的逻辑移动到动作/钩子方法(hook)里。

分页是很好的用例:分页算法可拆成抽象的不变部分与具体的可变部分。不变部分处理每页最大行数/页数等通用逻辑;可变部分则负责页眉/页脚等具体呈现。

几乎所有应用框架都或多或少用到了模板模式。我们用框架创建 GUI 应用时,通常从某个类继承并实现自定义行为。而在此之前,框架会调用一个模板方法,实现那些始终相同的流程,如绘制界面、事件循环、窗口缩放与居中等(参见 Python 3 Patterns, Recipes and Idioms,p.133)。

实现模板模式

下面实现一个横幅(banner)生成器。思路很简单:给函数一段文本,它返回带有样式(如在文本周围加点或加横线等)的横幅。生成器有一个默认样式,但也应允许自定义样式

generate_banner() 是我们的模板函数。它接收要展示的文本 msg,以及要应用的样式函数 stylegenerate_banner() 负责在样式化文本外部包上一层头部与尾部。头尾部这里用简单字符串演示,但完全可以改为调用更复杂的头/尾部生成函数:

def generate_banner(msg, style):
    print("-- start of banner --")
    print(style(msg))
    print("-- end of banner --nn")

dots_style() 样式会把 msg 首字母大写,并在前后各加 10 个点

def dots_style(msg):
    msg = msg.capitalize()
    ten_dots = "." * 10
    msg = f"{ten_dots}{msg}{ten_dots}"
    return msg

另一个样式 admire_style() 会把文本转成大写,并在每个字符之间加入一个感叹号:

def admire_style(msg):
    msg = msg.upper()
    return "!".join(msg)

第三个样式 cow_style()(作者最喜欢的)使用 cowpymilk_random_cow(),每次调用都会生成一个**随机的 ASCII 艺术“奶牛”**来“说话”:

def cow_style(msg):
    msg = cow.milk_random_cow(msg)
    return msg

main() 函数把 "happy coding" 依次用上述三种样式生成横幅并打印到标准输出:

def main():
    styles = (dots_style, admire_style, cow_style)
    msg = "happy coding"
    [generate_banner(msg, style) for style in styles]

示例完整代码回顾(ch05/template.py):

  • cowpy 导入所需函数;
  • 定义模板函数 generate_banner()
  • 定义 dots_style()
  • 定义 admire_style()cow_style()
  • 编写 main() 并调用。

运行 python ch05/template.py 可看到示例输出(注意:由于 cowpy 的随机性,你的 cow_style() 输出可能不同)。

image.png

你喜欢 cowpy 生成的艺术吗?我当然喜欢。作为练习,你可以创建你自己的样式并把它加入横幅生成器中。

另一个不错的练习是尝试实现你自己的 Template 示例。找出你写过的重复代码,看看这个模式是否适用。

其他行为型设计模式

关于 GoF 目录中的其他行为型设计模式,还有 Mediator(中介者)模式Visitor(访问者)模式

  • 中介者模式通过封装对象之间的交互来促进松耦合。在该模式中,各对象不直接通信,而是通过一个中介者对象进行沟通。中介者像一个中心枢纽,协调参与对象之间的通讯,适合在交互复杂时实现解耦与管理。
  • 访问者模式适用于复杂场景,它将算法与其作用的对象结构分离。通过在不修改元素类的前提下引入新操作,访问者模式为面向对象系统带来灵活性与可扩展性

本书不展开这两种模式,因为它们并非 Python 开发者的常用工具。Python 自带的特性与库,往往能在不实现这些模式的情况下,达成松耦合可扩展的目标。例如,可以使用 asyncio 等库的事件驱动编程替代“通过中介者对象进行通信”;再比如,借助一等函数装饰器上下文管理器,就能封装算法与操作,而无需显式的访问者对象。

总结

本章讨论了行为型设计模式

首先介绍了责任链(Chain of Responsibility)模式,它简化复杂处理流的管理,有助于提升设计的灵活性可维护性

接着是命令(Command)模式,它把请求封装成对象,从而让我们可以用队列、请求与操作来参数化客户端,并支持可撤销的操作。尽管“撤销”是命令模式最常被强调的特性,但它的用途远不止于此——凡是可在运行时按需执行的操作,都是命令模式的好候选。

然后我们讲了观察者(Observer)模式,它通过发布-订阅帮助关注点分离,增强发布者与订阅者之间的解耦。观察者与主题松耦合,且可动态添加/移除

随后介绍状态(State)模式——本质上是为特定软件工程问题实现一个或多个状态机。状态机任一时刻只有一个活动状态迁移是从当前状态切换到新状态;在迁移之前/之后执行动作是常见做法。状态机可用状态图直观表示,广泛用于计算与非计算领域。我们用 state_machine 模块实现了一个进程的状态机,该模块简化了状态机创建与迁移前/后动作定义。

之后是解释器(Interpreter)模式,用于为高级用户与领域专家提供类似编程的框架,而不暴露完整编程语言的复杂性。这通过实现DSL(表达力有限、面向特定领域的计算机语言)来达成。解释器主要与内部 DSL相关。尽管解释器模式通常不涉及解析,我们在实现中使用 pyparsing 构建了一个智能家居 DSL,并看到借助良好的解析工具,可用模式匹配轻松解释结果。

接下来是策略(Strategy)模式,当我们希望透明地为同一问题使用多种解法时非常合适。不存在对所有输入与场景都完美的单一算法;借助策略,我们可以动态决定在每种情况下使用哪一种。Python 的一等函数让策略实现更简洁;我们以“检测单词中字符是否唯一”的两种算法为例进行了演示。

然后我们看了备忘录(Memento)模式,用于在需要时存储并恢复对象状态。它为实现撤销能力提供了高效方案;另一个常见用法是OK/Cancel 对话框:若用户取消则恢复初始状态。示例中我们用标准库 pickle(简化版示例,注意其通用安全性问题)来保存/恢复数据对象的过往状态。

接着介绍迭代器(Iterator)模式,它为遍历序列与对象集合提供了优雅高效的方式。现实中,凡是“对一组东西逐个取用”,本质上就是迭代。Python 将迭代器作为语言特性提供:内建容器(如列表、字典)可直接迭代;我们也可以遵循迭代器协议(实现 __iter__() / __next__())自定义可迭代与迭代器类。示例里我们实现了一个足球队的迭代。

随后展示了如何用模板(Template)模式在实现结构相似的算法时消除冗余。我们用“员工日常作息”类比模板模式,并提到 Python 库中两个采用模板思想的例子与通用适用场景。最后实现了一个横幅生成器,用模板函数承载不变流程,并以样式函数承载可变部分。

此外,还有中介者访问者两种行为型模式,但在 Python 开发中并不常用,故未展开。

下一章我们将探索架构型设计模式,它们用于解决常见的架构层面问题。