精通-Python-设计模式第二版-全-

精通 Python 设计模式第二版(全)

原文:zh.annas-archive.org/md5/14c6e702517d3033da0a5969e2cd44c1

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

使用这本综合指南,在 Python 编程语言的背景下探索设计原则和设计模式的世界。了解经典和现代设计模式以及如何使用它们来解决作为 Python 开发人员或软件架构师在日常工作中遇到的问题。

通过代码示例、现实世界的案例研究和详细的解决方案实现,本书是希望提升编程技能的 Python 开发者的必读之作。本书由拥有二十多年经验的 Python 专家合著,新版本扩大了范围,涵盖了更多设计模式类别。深入了解现代软件设计的重要模式,如并发、异步和性能模式,包括创建型、结构型、行为型、架构型和其他模式。学习如何在事件处理系统、并发、分布式系统和测试等各个领域应用这些模式。本书还介绍了 Python 反模式,帮助你避免常见的陷阱。

无论你是在开发用户界面、Web 应用、API、数据管道还是 AI 模型,本书都为你提供了构建健壮和可维护软件的知识。

本书采用实践导向的方法,为每个设计模式提供代码示例。每一章都包括逐步指导来测试代码,使其成为互动式学习体验。对于每个设计原则或模式,本书至少提供一个现实世界的例子,这个例子可能基于 Python,也可能不是,以及至少一个基于 Python 的例子。

本书面向对象

本书面向希望深化对设计模式及其在各种类型项目中应用理解的 Python 开发者。本书专注于中级和高级 Python 程序员,还包括入门章节,使其对相对较新的语言用户也易于理解。无论你是 Web 开发者、数据工程师还是 AI 专家,本书都提供了基于现实世界例子和几十年经验的软件设计最佳实践的有价值见解。它也是软件架构师和团队领导者的优秀资源,他们希望提高项目中的代码质量和可维护性。

本书涵盖内容

第一章基础设计原则,涵盖了封装、组合、面向接口编程和松耦合的原则,以帮助你创建更适应性和可维护的系统。

第二章SOLID 原则,提供了设计健壮、可维护和可扩展软件的指南。这些原则中的每一个都有助于创建干净和可适应的代码。

第三章创建型设计模式,探讨了帮助通过控制要实例化的类来管理对象创建的模式。

第四章结构设计模式,提供了通过识别建立实体之间关系的简单方法来促进设计过程的见解。本章深入探讨了六个基本结构模式,为您提供高效且优雅地构建代码的技能。

第五章行为设计模式,分享了关注对象交互和职责的模式,促进有效沟通和灵活分配职责。本章探讨了关键模式,如策略、观察者和命令,展示了它们如何简化对象协作并增强代码的适应性。

第六章架构设计模式,深入探讨了提供解决常见架构问题模板的模式,促进可扩展、可维护和可重用系统的开发。

第七章并发和异步模式,探讨了有助于开发既快速又用户友好的应用程序的模式,尤其是在具有大量 I/O 操作或重大计算工作的环境中。

第八章性能模式,提供了针对常见瓶颈和优化挑战的指导,提供了经过验证的方法来提高执行时间、减少内存使用并有效扩展。

第九章分布式系统模式,展示了使开发者能够构建健壮分布式系统的模式,从管理节点间的通信到确保容错性和一致性。

第十章测试模式,介绍了有助于隔离组件、提高测试可靠性和促进代码重用性的模式。

第十一章Python 反模式,探讨了虽然不一定错误,但往往导致代码效率低下、可读性差和/或可维护性差的常见编程实践。您将学会理解和避免这些陷阱。

要充分利用这本书

使用装有最新版 Windows、Linux 或 macOS 的机器。

安装 Python 3.12。从 Python 安装中创建一个虚拟环境也很有用,这样当您添加一些章节所需的第三方模块时,就不会污染您的全局 Python。这是使用 Python 提高生产力的基本最佳实践,您将在互联网上找到许多解释如何做到这一点的资源。

在您的机器上安装和使用 Docker。这将有助于满足某些外部软件服务或工具的需求,例如 LocalStack(用于第六章)和 Redis 服务器(用于第八章)。

本书涵盖的软件/硬件 操作系统要求
Python 3.12 Windows、macOS 或 Linux
MyPy 1.10.0
Docker
Redis-server 6.2.6
LocalStack 3.4.0

如果你使用的是这本书的数字版,我们建议你亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助你避免与代码复制和粘贴相关的任何潜在错误。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Mastering-Python-Design-Patterns-Third-Edition。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他丰富的图书和视频的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

在整本书中使用了多种文本约定和格式化特定要求。

大部分代码已经自动格式化

格式化使用的是 Black 工具,这是 Python 开发者为了提高生产力而通常使用的。因此,它可能看起来并不完全像你自己写的代码。但是它是有效的;它是一个 PEP 8 兼容的代码。目标是提高代码片段的可读性。

因此,代码文件以及书页上的某些代码片段可能看起来如下:

State = Enum(
    "State",
    "NEW RUNNING SLEEPING RESTART ZOMBIE",
)

另一个例子可能是以下内容:

msg = (
    f"trying to create process '{name}' "
    f"for user '{user}'"
)
print(msg)

书页上的代码片段可能会缩短

为了提高可读性,当函数或类的文档字符串(docstring)过长时,我们从书中的代码片段中移除它。

当某些代码(类或函数)太长而无法在章节页面上显示时,我们可能会缩短它,并指导读者查看文件中的完整代码。

注意

如果长命令因多行而分散(以‘/’字符为分隔符),你可以重新格式化长命令文本,移除‘/’字符,以确保命令在终端中正确解释。

其他约定

Code in text: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“使用log方法定义Logger接口。”

代码块按照以下方式设置:

class MyInterface(ABC):
    @abstractmethod
    def do_something(self, param: str):
        pass

任何命令行输入或输出都按照以下方式编写:

python3.12 –m pip install -–user mypy

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“这是面向对象编程(OOP)的核心概念之一,它使单个接口能够表示不同的类型。”

小贴士或重要注意事项

它看起来像这样。

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请通过 customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。

错误清单:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享你的想法

一旦您阅读了《精通 Python 设计模式》,我们很乐意听到您的想法!请点击此处直接转到该书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

你喜欢在路上阅读,但又无法携带你的印刷书籍到处走?

你的电子书购买是否与您选择的设备不兼容?

不要担心,现在,随着每本 Packt 书籍,你都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何地点、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠不会就此停止,您还可以获得独家折扣、时事通讯和每日收件箱中的精彩免费内容。

按照以下简单步骤获取优惠:

  1. 扫描二维码或访问以下链接

packt.link/free-ebook/9781837639618

  1. 提交你的购买证明

  2. 就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件地址

第一部分:从原则开始

这一部分向您介绍了基础软件设计原则以及建立在它们之上的 S.O.L.I.D.原则。本部分包括以下章节:

  • 第一章, 基础设计原则

  • 第二章, SOLID 原则

第一章:基础设计原则

设计原则是任何良好架构的软件的基础。它们作为指导之光,帮助开发者导航创建可维护、可扩展和健壮应用程序的道路,同时避免不良设计带来的陷阱。

在本章中,我们将探讨所有开发者都应该了解并应用于其项目的核心设计原则。我们将探讨四个基础原则。第一个,“封装变化”,教您如何隔离代码中易变的部分,使修改和扩展应用程序变得更加容易。接下来,“优先使用组合”,让您理解为什么通常从简单对象组装复杂对象比继承功能更好。第三个,“面向接口编程”,展示了面向接口而非具体类编码的力量,增强了灵活性和可维护性。最后,通过“松耦合”原则,您将掌握减少组件之间依赖关系的重要性,使代码更容易重构和测试。

在本章中,我们将涵盖以下主要主题:

  • 遵循“封装变化”原则

  • 遵循“优先使用组合而非继承”原则

  • 遵循“面向接口而非实现编程”原则

  • 遵循“松耦合”原则

到本章结束时,您将对这些原则及其在 Python 中的实现有一个扎实的理解,为本书的其余部分打下基础。

技术要求

对于本书中的章节,您需要一个运行中的 Python 3.12 环境或在某些章节的某些特殊情况中,3.11 版本。

此外,通过运行以下命令安装 Mypy 静态类型检查器(www.mypy-lang.org):

python3.12 –m pip install -–user mypy

示例可以在以下 GitHub 仓库中找到:github.com/PacktPublishing/Mastering-Python-Design-Patterns-Third-Edition

关于 Python 可执行文件

在整本书中,我们将引用 Python 可执行文件来执行代码示例,作为 python3.12python。根据您的具体环境、实践和/或工作流程进行适配。

遵循“封装变化”原则

软件开发中最常见的挑战之一是处理变化。需求演变、技术进步,以及用户需求也会发生变化。因此,编写能够适应变化而不会在您的程序或应用程序中引起连锁修改的代码至关重要。这就是“封装变化”原则发挥作用的地方。

这是什么意思?

这一原则背后的思想很简单:隔离您代码中最可能发生变化的部分,并将它们封装起来。通过这样做,您创建了一个保护屏障,保护代码的其他部分免受这些可能发生变化元素的影响。这种封装允许您在不影响其他部分的情况下对系统的一部分进行更改。

优点

封装变化的部分提供了几个好处,主要包括以下:

  • 易于维护:当需要更改时,您只需修改封装的部分,从而降低在其他地方引入错误的风险

  • 增强灵活性:封装的组件可以轻松交换或扩展,提供更适应的架构

  • 提高可读性:通过隔离变化元素,您的代码变得更加有组织,更容易理解

实现封装的技术

正如我们所介绍的,封装有助于数据隐藏,仅暴露必要的功能。在这里,我们将介绍增强 Python 中封装的关键技术:多态性和 getterssetters 技术。

多态性

在编程中,多态性允许将不同类的对象视为公共超类对象。它是 面向对象编程 OOP 的核心概念之一,它使单个接口能够表示不同类型。多态性允许实现优雅的软件设计模式,如策略模式,并且是实现 Python 中干净、可维护代码的一种方式。

Getters and Setters

这些是在一个类中使用的特殊方法,它们允许对属性值进行受控访问。getters 允许读取属性的值,而 setters 允许修改它们。通过使用这些方法,您可以添加验证逻辑或副作用,如记录日志,从而遵循封装的原则。它们提供了一种控制和保护对象状态的方法,并且在您想要封装从其他实例变量派生的复杂属性时特别有用。

还有更多。为了补充 getterssetters 技术,Python 提供了一种更优雅的方法,称为 property 技术。这是 Python 的内置功能,允许您无缝地将属性访问转换为方法调用。使用属性,您可以在不显式定义 gettersetter 方法的情况下,确保对象在不受正确或有害操作的情况下保持其内部状态。

@property 装饰器允许您定义一个方法,当访问属性时自动调用,有效地充当 getter。同样,@attribute_name.setter 装饰器允许您定义一个方法,充当 setter,在您尝试更改属性值时调用。这样,您可以直接在这些方法中嵌入验证或其他操作,使代码更加简洁。

通过使用属性技术,你可以实现与传统的获取器设置器相同级别的数据封装和验证,但以一种更符合 Python 设计哲学的方式。它允许你编写不仅功能性强,而且干净、易于阅读的代码,从而增强封装和 Python 程序的整体质量。

接下来,我们将通过示例更好地理解这些技术。

举例——使用多态进行封装

多态是实现变化行为封装的强大方式。让我们通过一个支付处理系统的例子来看一下,在这个系统中,支付方式选项可以变化。在这种情况下,你可能会将每种支付方式封装在其自己的类中:

  1. 你首先定义支付方法的基类,提供一个process_payment()方法,每个具体的支付方法都将实现它。这就是我们封装变化的部分——支付处理逻辑。这部分代码如下:

    class PaymentBase:
        def __init__(self, amount: int):
            self.amount: int = amount
        def process_payment(self):
            pass
    
  2. 接下来,我们将介绍CreditCardPayPal类,它们继承自PaymentBase,每个类都提供了自己的process_payment实现。这是一种经典的多态方式,因为你可以将CreditCardPayPal对象视为它们共同超类实例。代码如下:

    class CreditCard(PaymentBase):
        def process_payment(self):
            msg = f"Credit card payment: {self.amount}"
            print(msg)
    class PayPal(PaymentBase):
        def process_payment(self):
            msg = f"PayPal payment: {self.amount}"
            print(msg)
    
  3. 为了使测试我们刚刚创建的类成为可能,让我们添加一些代码,为每个对象调用process_payment()。当你使用这些类时,多态的美丽之处显而易见,如下所示:

    if __name__ == "__main__":
        payments = [CreditCard(100), PayPal(200)]
        for payment in payments:
            payment.process_payment()
    

本例的完整代码(ch01/encapsulate.py)如下:

class PaymentBase:
    def __init__(self, amount: int):
        self.amount: int = amount
    def process_payment(self):
        pass
class CreditCard(PaymentBase):
    def process_payment(self):
        msg = f"Credit card payment: {self.amount}"
        print(msg)
class PayPal(PaymentBase):
    def process_payment(self):
        msg = f"PayPal payment: {self.amount}"
        print(msg)
if __name__ == "__main__":
    payments = [CreditCard(100), PayPal(200)]
    for payment in payments:
        payment.process_payment()

要测试代码,请运行以下命令:

python3.12 ch01/encapsulate.py

你应该得到以下输出:

Credit card payment: 100
PayPal payment: 200

如你所见,当支付方式改变时,程序会适应以产生预期的结果。

通过封装变化的部分——在这里是支付方式——你可以轻松地添加新选项或修改现有选项,而不会影响核心支付处理逻辑。

举例——使用属性进行封装

让我们定义一个Circle类,并展示如何使用 Python 的@property技术为其radius属性创建一个获取器和一个设置器

注意,底层属性实际上会被称为_radius,但它被隐藏/保护在名为radius属性后面。

让我们一步步编写代码:

  1. 我们首先定义Circle类及其初始化方法,其中我们将_radius属性初始化如下:

    class Circle:
        def __init__(self, radius: int):
            self._radius: int = radius
    
  2. 我们添加半径属性:一个radius()方法,其中我们从底层属性返回值,使用@property装饰器装饰,如下所示:

        @property
        def radius(self):
            return self._radius
    
  3. 我们添加半径设置器部分:另一个radius()方法,其中我们在验证检查后实际修改底层属性,因为我们不希望允许半径为负值;此方法由特殊的@radius.setter装饰器装饰。这部分代码如下:

        @radius.setter
        def radius(self, value: int):
            if value < 0:
                raise ValueError("Radius cannot be negative!")
            self._radius = value
    
  4. 最后,我们添加一些将帮助我们测试类的代码,如下所示:

    if __name__ == "__main__":
        circle = Circle(10)
        print(f"Initial radius: {circle.radius}")
        circle.radius = 15
        print(f"New radius: {circle.radius}")
    

此示例的完整代码(ch01/encapsulate_bis.py)如下所示:

class Circle:
    def __init__(self, radius: int):
        self._radius: int = radius
    @property
    def radius(self):
        return self._radius
    @radius.setter
    def radius(self, value: int):
        if value < 0:
            raise ValueError("Radius cannot be negative!")
        self._radius = value
if __name__ == "__main__":
    circle = Circle(10)
    print(f"Initial radius: {circle.radius}")
    circle.radius = 15
    print(f"New radius: {circle.radius}")

要测试此示例,请运行以下命令:

python3.12 ch01/encapsulate_bis.py

您应该得到以下输出:

Initial radius: 10
New radius: 15

在这个第二个示例中,我们看到了如何封装圆的半径组件,以便在需要时更改技术细节,而不会破坏类。例如,setter 的验证代码可以演变。我们甚至可以更改基础属性 _radius,而我们的代码用户的行为将保持不变。

遵循“优先使用组合而非继承”原则

在面向对象编程中,通过继承创建复杂的类层次结构是很诱人的。虽然继承有其优点,但它可能导致代码紧密耦合,难以维护和扩展。这就是“优先使用组合而非继承”原则发挥作用的地方。

这是什么意思?

此原则建议您应该优先从更简单的部分组合对象,而不是从基类继承功能。换句话说,通过组合更简单的对象来构建复杂对象。

优点

选择组合而非继承提供了一些优点:

  • 灵活性:组合允许您在运行时更改对象的行为,使代码更具适应性

  • 可重用性:较小的、简单的对象可以在应用程序的不同部分重用,从而促进代码的可重用性

  • 易于维护:使用组合,您可以轻松地替换或更新单个组件,而不会影响整体系统,避免边界效应

组合的技术

在 Python 中,组合通常通过面向对象编程实现,即在类中包含其他类的实例。这有时被称为被组合的类和被包含的类之间的“具有”关系。Python 通过不需要显式类型声明,特别容易使用组合。您可以通过在类的 __init__ 方法中实例化它们或作为参数传递来包含其他对象。

例子 - 使用发动机组合汽车

在 Python 中,您可以通过在您的类中包含其他类的实例来实现组合。例如,考虑一个包含 Engine 类实例的 Car 类:

  1. 让我们先定义如下所示的 Engine 类,其中包含其 start 方法:

    class Engine:
        def start(self):
            print("Engine started")
    
  2. 然后,让我们定义如下所示的 Car 类:

    class Car:
        def __init__(self):
            self.engine = Engine()
        def start(self):
            self.engine.start()
            print("Car started")
    
  3. 最后,在程序执行时,添加以下代码行以创建 Car 类的实例,并在该实例上调用 start 方法:

    if __name__ == "__main__":
        my_car = Car()
        my_car.start()
    

此示例的完整代码(ch01/composition.py)如下所示:

class Engine:
    def start(self):
        print("Engine started")
class Car:
    def __init__(self):
        self.engine = Engine()
    def start(self):
        self.engine.start()
        print("Car started")
if __name__ == "__main__":
    my_car = Car()
    my_car.start()

要测试代码,请运行以下命令:

python3.12 ch01/composition.py

您应该得到以下输出:

Engine started
Car started

正如您在这个示例中所看到的,Car 类由一个 Engine 对象组成,这是通过 self.engine = Engine() 这一行实现的,这使得您能够轻松地更换发动机类型,而无需更改 Car 类本身。

遵循“面向接口而非实现”原则

在软件设计中,很容易陷入如何实现一个功能的细节。然而,过分关注实现细节可能导致代码紧密耦合且难以修改。面向接口,而非实现的原则为解决这个问题提供了解决方案。

这是什么意思?

接口定义了类的一个契约,指定了一组必须实现的方法。

这个原则鼓励你针对接口而不是具体类进行编码。通过这样做,你将代码从提供所需行为的特定类解耦,使得在不影响系统其他部分的情况下更容易交换或扩展实现。

优点

面向接口编程提供了几个优点:

  • 灵活性:你可以轻松地在不同的实现之间切换,而无需更改使用它们的代码

  • 可维护性:失去特定实现中的代码使得更新或替换组件变得更加容易

  • 可测试性:接口使得编写单元测试更加简单,因为在测试期间你可以轻松地模拟接口

接口技术

在 Python 中,接口可以通过两种主要技术实现:抽象基类ABCs)和协议。

抽象基类

abc模块提供的ABCs(抽象基类),允许你定义必须由任何具体(即非抽象)子类实现的抽象方法

让我们通过一个示例来理解这个概念,我们将定义一个抽象类(作为接口)然后使用它:

  1. 首先,我们需要按照以下方式导入ABC类和abstractmethod装饰器函数:

    from abc import ABC, abstractmethod
    
  2. 然后,我们定义接口类如下:

    class MyInterface(ABC):
        @abstractmethod
        def do_something(self, param: str):
            pass
    
  3. 现在,为该接口定义一个具体类;它从接口类继承并提供了do_something方法的实现,如下所示:

    class MyClass(MyInterface):
        def do_something(self, param: str):
            print(f"Doing something with: '{param}'")
    
  4. 为了测试目的,添加以下行:

    if __name__ == "__main__":
        MyClass().do_something("some param")
    

完整的代码(ch01/abstractclass.py)如下:

from abc import ABC, abstractmethod
class MyInterface(ABC):
    @abstractmethod
    def do_something(self, param: str):
        pass
class MyClass(MyInterface):
    def do_something(self, param: str):
        print(f"Doing something with: '{param}'")
if __name__ == "__main__":
    MyClass().do_something("some param")

要测试代码,请运行以下命令:

python3.12 ch01/abstractclass.py

你应该得到以下输出:

Doing something with: 'some param'

现在你已经知道如何在 Python 中定义接口和实现该接口的具体类。

协议

通过typing模块在 Python 3.8 中引入的协议,比 ABCs 提供了更灵活的方法,称为结构化鸭子类型,其中如果对象具有某些属性或方法,则认为它是有效的,而不管其实际继承关系如何。

与在运行时确定类型兼容性的传统鸭子类型不同,结构化鸭子类型允许在编译时进行类型检查。这意味着你可以在代码甚至运行之前(例如在 IDE 中)捕获类型错误,使你的程序更加健壮且更容易调试。

使用协议的关键优势是它们关注对象能做什么,而不是它是什么。换句话说,如果一个对象像鸭子走路,像鸭子嘎嘎叫,那么它就是一只鸭子,无论它的实际继承层次结构如何。这在像 Python 这样的动态类型语言中尤其有用,其中对象的行为比其实际类型更重要。

例如,你可以定义一个Drawable协议,它需要一个draw()方法。任何实现此方法的类都会隐式满足协议,而无需明确从它继承。

这里有一个快速示例来说明这个概念。假设你需要一个名为Flyer的协议,它需要一个fly()方法。你可以这样定义它:

from typing import Protocol
class Flyer(Protocol):
    def fly(self) -> None:
        ...

就这样!现在,任何具有fly()方法的类都会被认为是Flyer,无论它是否明确地从Flyer类继承。这是一个强大的功能,允许你编写更通用和可重用的代码,并遵循我们之前在遵循“优先使用组合而非继承”原则部分讨论的原则。

在后面的示例中,我们将看到协议的实际应用。

举例说明 – 不同类型的记录器

使用 ABCs,让我们创建一个允许不同类型日志记录机制的日志接口。以下是实现方式:

  1. abc导入所需的模块:

    from abc import ABC, abstractmethod
    
  2. 使用log方法定义Logger接口:

    class Logger(ABC):
        @abstractmethod
        def log(self, message: str):
            pass
    
  3. 然后,定义两个具体的类,它们实现了Logger接口,用于两种不同的Logger类型:

    class ConsoleLogger(Logger):
        def log(self, message: str):
            print(f"Console: {message}")
    class FileLogger(Logger):
        def log(self, message: str):
            with open("log.txt", "a") as f:
                f.write(f"File: {message}\n")
    
  4. 接下来,为了使用每种类型的记录器,请定义一个如下所示的功能:

    def log_message(logger: Logger, message: str):
        logger.log(message)
    

    注意,该函数将其第一个参数作为类型为Logger的对象,这意味着一个实现了Logger接口的具体类的实例(即ConsoleLoggerFileLogger)。

  5. 最后,添加测试代码所需的行,如下调用log_message函数:

    if __name__ == "__main__":
        log_message(ConsoleLogger(), "A console log.")
        log_message(FileLogger(), "A file log.")
    

这个示例的完整代码(ch01/interfaces.py)如下所示:

from abc import ABC, abstractmethod
class Logger(ABC):
    @abstractmethod
    def log(self, message: str):
        pass
class ConsoleLogger(Logger):
    def log(self, message: str):
        print(f"Console: {message}")
class FileLogger(Logger):
    def log(self, message: str):
        with open("log.txt", "a") as f:
            f.write(f"File: {message}\n")
def log_message(logger: Logger, message: str):
    logger.log(message)
if __name__ == "__main__":
    log_message(ConsoleLogger(), "A console log.")
    log_message(FileLogger(), "A file log.")

要测试代码,请运行以下命令:

python3.12 ch01/interfaces.py

你应该得到以下输出:

Console: A console log.

除了那个输出之外,查看你运行命令的文件夹,你会发现一个名为log.txt的文件已经被创建,其中包含以下行:

File: A file log.

正如你刚才在log_message函数中看到的,你可以轻松地在不同的日志记录机制之间切换,而无需更改函数本身。

举例说明 – 使用协议的不同类型的记录器

让我们用协议方式重新审视之前的示例:

  1. 首先,我们需要如下导入Protocol类:

    from typing import Protocol
    
  2. 然后,通过从Protocol类继承来定义Logger接口,如下所示:

    class Logger(Protocol):
        def log(self, message: str):
            ...
    

    并且其余的代码保持不变。

因此,完整的代码(ch01/interfaces_bis.py)如下所示:

from typing import Protocol
class Logger(Protocol):
    def log(self, message: str):
        ...
class ConsoleLogger:
    def log(self, message: str):
        print(f"Console: {message}")
class FileLogger:
    def log(self, message: str):
        with open("log.txt", "a") as f:
            f.write(f"File: {message}\n")
def log_message(logger: Logger, message: str):
    logger.log(message)
if __name__ == "__main__":
    log_message(ConsoleLogger(), "A console log.")
    log_message(FileLogger(), "A file log.")

要根据我们定义的协议检查代码的静态类型,请运行以下命令:

mypy ch01/interfaces_bis.py

你应该得到以下输出:

Success: no issues found in 1 source file

要测试代码,请运行以下命令:

python3.12 ch01/interfaces_bis.py

你应该得到与运行上一个版本相同的结果——换句话说,创建的log.txt文件和 shell 中的以下输出:

Console: A console log.

这是正常的,因为我们唯一改变的是定义接口的方式。而且,接口(协议)的效果在运行时没有强制执行,这意味着它不会改变代码执行的实际情况。

遵循松散耦合原则

随着软件的复杂性增加,其组件之间的关系可能会变得复杂,导致系统难以理解、维护和扩展。松散耦合原则旨在减轻这一问题。

这是什么意思?

松散耦合指的是最小化程序不同部分之间的依赖关系。在松散耦合系统中,组件是独立的,并通过定义良好的接口进行交互,这使得修改一个部分而不影响其他部分变得更容易。

优点

松散耦合提供了几个优点:

  • 可维护性:由于依赖项较少,更新或替换单个组件更容易

  • 可扩展性:松散耦合的系统可以更容易地通过添加新功能或组件来扩展

  • 可测试性:独立的组件更容易在隔离状态下进行测试,从而提高软件的整体质量

松散耦合的技术

实现松散耦合的两个主要技术是依赖注入观察者模式。依赖注入允许组件从外部源接收其依赖项,而不是创建它们,这使得交换或模拟这些依赖项更容易。另一方面,观察者模式允许一个对象发布其状态的变化,以便其他对象可以相应地做出反应,而无需紧密绑定在一起。

这两种技术都旨在减少组件之间的相互依赖性,使你构建的系统更加模块化,更容易管理。

我们将在第五章中详细讨论观察者模式行为设计模式。现在,让我们通过一个例子来了解如何使用依赖注入技术。

举例来说,一个消息服务

在 Python 中,你可以通过使用依赖注入来实现松散耦合。让我们看看一个涉及MessageService类的简单例子:

  1. 首先,我们定义MessageService类如下:

    class MessageService:
        def __init__(self, sender):
            self.sender = sender
        def send_message(self, message):
            self.sender.send(message)
    

    如你所见,该类将通过传递一个发送对象给它来初始化;该对象有一个send方法,允许发送消息。

  2. 第二,让我们定义一个EmailSender类:

    class EmailSender:
        def send(self, message):
            print(f"Sending email: {message}")
    
  3. 第三,让我们定义一个SMSSender类:

    class SMSSender:
        def send(self, message):
            print(f"Sending SMS: {message}")
    
  4. 现在我们可以使用一个EmailSender对象实例化MessageService并使用它来发送消息。我们也可以使用一个SMSSender对象来实例化MessageService。我们添加了以下代码来测试这两个操作:

    if __name__ == "__main__":
        email_service = MessageService(EmailSender())
        email_service.send_message("Hello via Email")
        sms_service = MessageService(SMSSender())
        sms_service.send_message("Hello via SMS")
    

此示例的完整代码,保存在ch01/loose_coupling.py文件中,如下所示:

class MessageService:
    def __init__(self, sender):
        self.sender = sender
    def send_message(self, message: str):
        self.sender.send(message)
class EmailSender:
    def send(self, message: str):
        print(f"Sending email: {message}")
class SMSSender:
    def send(self, message: str):
        print(f"Sending SMS: {message}")
if __name__ == "__main__":
    email_service = MessageService(EmailSender())
    email_service.send_message("Hello via Email")
    sms_service = MessageService(SMSSender())
    sms_service.send_message("Hello via SMS")

要测试代码,请运行以下命令:

python3.12 ch01/loose_coupling.py

你应该得到以下输出:

Sending email: Hello via Email
Sending SMS: Hello via SMS

在这个例子中,MessageService通过依赖注入与EmailSenderSMSSender松散耦合。这允许你轻松地在不同的发送机制之间切换,而无需修改MessageService类。

摘要

我们从本书开始,介绍了开发者应该遵循的基础设计原则,以编写可维护、灵活和健壮的软件。从封装变化的部分到偏好组合、面向接口编程以及追求松散耦合,这些原则为任何 Python 开发者提供了一个强大的基础。

正如你所见,这些原则不仅仅是理论上的构建,而是可以显著提高你代码质量的实用指南。它们为接下来要讨论的内容奠定了基础:深入探讨更多专门化的原则集合,这些原则指导着面向对象的设计。

在下一章中,我们将深入探讨 SOLID 原则,这是一组旨在使软件设计更易于理解、灵活和可维护的五个设计原则。

第二章:SOLID 原则

在软件工程的世界里,原则和最佳实践是构建健壮、可维护和高效代码库的基石。在前一章中,我们介绍了每个开发者都需要遵循的基础原则。

在本章中,我们继续探讨设计原则,重点关注由罗伯特·C·马丁提出的 SOLID,这是一个代表他提出的五个设计原则的首字母缩略词,旨在使软件更易于理解、灵活和可维护。

本章中,我们将涵盖以下主要内容:

  • 单一职责原则SRP

  • 开放封闭原则OCP

  • 里氏替换原则LSP

  • 接口隔离原则ISP

  • 依赖倒置原则DIP

到本章结束时,你将理解这五个额外的设计原则以及如何在 Python 中应用它们。

技术要求

请参阅第一章中提出的要求。

SRP

SRP 是软件设计中的一个基本概念。它主张在定义一个类以提供功能时,该类应该只有一个存在的理由,并且只负责功能的一个方面。用更简单的话说,它提倡每个类应该有一个工作或职责,并且这个工作应该封装在该类中。

因此,通过遵循 SRP,你实际上是在努力使类专注于功能、具有凝聚力和专业化。这种方法在提高代码库的可维护性和可理解性方面发挥着至关重要的作用。当每个类都有一个明确且单一的目的时,它就更容易管理、理解和扩展。

当然,你没有义务遵循 SRP。但了解这个原则,并带着这个想法思考你的代码,将随着时间的推移提高你的代码库。

在实践中,应用单一职责原则(SRP)通常会导致更小、更专注的类,这些类可以组合和组合以创建复杂的系统,同时保持清晰和有序的结构。

注意

SRP 并非关于最小化类中的代码行数,而是确保一个类只有一个改变的理由,减少在修改时产生意外副作用的可能性。

让我们通过一个小例子来使事情更清晰。

跟随 SRP 的软件设计示例

让我们想象一些可以在许多不同类型的应用程序中使用的代码,例如内容或文档管理工具或专门的 Web 应用程序,这些应用程序包括生成 PDF 文件并将其保存到磁盘的功能。为了帮助理解 SRP(单一职责原则),让我们考虑一个初始版本,其中代码不遵循此原则。在这种情况下,开发者可能会定义一个处理报告的类,称为Report,并以使其负责生成报告并保存到文件的方式实现它。此类典型的代码可能如下所示:

class Report:
    def __init__(self, content):
        self.content = content
    def generate(self):
        print(f"Report content: {self.content}")
    def save_to_file(self, filename):
        with open(filename, 'w') as file:
            file.write(self.content)

如您所见,Report类有两个职责。首先,生成报告,然后,将报告内容保存到文件。

当然,这是可以的。但设计原则鼓励我们考虑为未来改进事物,因为需求会演变,代码会增长以处理复杂性和变化。在这里,SRP(单一职责原则)教导我们分离事物。为了遵循 SRP,我们可以重构代码以使用两个不同的类,每个类将各自有一个职责,如下所示:

  1. 创建第一个类,负责生成报告内容:

    class Report:
        def __init__(self, content: str):
            self.content: str = content
        def generate(self):
            print(f"Report content: {self.content}")
    
  2. 创建一个第二类来处理将报告保存到文件的需求:

    class ReportSaver:
        def __init__(self, report: Report):
            self.report: Report = report
        def save_to_file(self, filename: str):
            with open(filename, 'w') as file:
                file.write(self.report.content)
    
  3. 为了确认我们的重构版本可以正常工作,让我们添加以下代码以便立即进行测试:

    if __name__ == "__main__":
        report_content = "This is the content."
        report = Report(report_content)
        report.generate()
        report_saver = ReportSaver(report)
        report_saver.save_to_file("report.txt")
    

为了总结,以下是完整的代码,保存在ch02/srp.py文件中:

class Report:
    def __init__(self, content: str):
        self.content: str = content
    def generate(self):
        print(f"Report content: {self.content}")
class ReportSaver:
    def __init__(self, report: Report):
        self.report: Report = report
    def save_to_file(self, filename: str):
        with open(filename, "w") as file:
            file.write(self.report.content)
if __name__ == "__main__":
    report_content = "This is the content."
    report = Report(report_content)
    report.generate()
    report_saver = ReportSaver(report)
    report_saver.save_to_file("report.txt")

要查看代码的结果,请运行以下命令:

python ch02/srp.py

您将得到以下输出:

report.txt file has been created. So, everything works as expected.
			As you can see, by following the SRP, you can achieve cleaner, more maintainable, and adaptable code, which contributes to the overall quality and longevity of your software projects.
			OCP
			The OCP is another fundamental principle in software design. It emphasizes that software entities, such as classes and modules, should be open for extension but closed for modification. What does that mean? It means that once a software entity is defined and implemented, it should not be changed to add new functionality. Instead, the entity should be extended through inheritance or interfaces to accommodate new requirements and behaviors.
			When thinking about this principle and if you have some experience writing code for non-trivial programs, you can see how it makes sense, since modifying an entity introduces a risk of breaking some other part of the code base relying on it.
			The OCP provides a robust foundation for building flexible and maintainable software systems. It allows developers to introduce new features or behaviors without altering the existing code base. By adhering to the OCP, you can minimize the risk of introducing bugs or unintended side effects when making changes to your software.
			An example of design following the OCP
			Consider a `Rectangle` class defined for rectangle shapes. Let’s say we add a way to calculate the area of different shapes, maybe by using a function. The hypothetical code for the definition of both the class and the function could look like the following:

class Rectangle:

def init(self, width:float, height: float):

self.width: float = width

self.height: float = height

def calculate_area(shape) -> float:

if isinstance(shape, Rectangle):

return shape.width * shape.height


			Note
			This code is not in the example code files. It is a hypothetical idea to start with in our thinking, and not the code you would end up using. Keep reading.
			Given that code, if we want to add more shapes, we have to modify the `calculate_area` function. That is not ideal as we will keep coming back to change that code and that means more time testing things to avoid bugs.
			As we aim to become good at writing maintainable code, let’s see how we could improve that code by adhering to the OCP, while extending it to support another type of shape, the circle (using a `Circle` class):

				1.  Start by importing what we will need:

    ```

    import math

    from typing import Protocol

    ```py

    				2.  Define a `Shape` protocol for an interface providing a method for the shape’s area:

    ```

    class Shape(Protocol):

    def area(self) -> float:

    ...

    ```py

			Note
			Refer to *Chapter 1*, *Foundational Design Principles*, to understand Python’s `Protocol` concept and technique.

				1.  Define the `Rectangle` class, which conforms to the `Shape` protocol:

    ```

    class Rectangle:

    def __init__(self, width: float, height: float):

    self.width: float = width

    self.height: float = height

    def area(self) -> float:

    return self.width * self.height

    ```py

    				2.  Also define the `Circle` class, which also conforms to the `Shape` protocol:

    ```

    class Circle:

    def __init__(self, radius: float):

    self.radius: float = radius

    def area(self) -> float:

    return math.pi * (self.radius**2)

    ```py

    				3.  Implement the `calculate_area` function in such a way that adding a new shape won’t require us to modify it:

    ```

    def calculate_area(shape: Shape) -> float:

    return shape.area()

    ```py

    				4.  Add some code for testing the `calculate_area` function on the two types of shape objects:

    ```

    if __name__ == "__main__":

    rect = Rectangle(12, 8)

    rect_area = calculate_area(rect)

    print(f"Rectangle area: {rect_area}")

    circ = Circle(6.5)

    circ_area = calculate_area(circ)

    print(f"Circle area: {circ_area:.2f}")

    ```py

			The following is the complete code for this example, saved in the `ch02/ocp.py` file:

import math

from typing import Protocol

class Shape(Protocol):

def area(self) -> float:

...

class Rectangle:

def init(self, width: float, height: float):

self.width: float = width

self.height: float = height

def area(self) -> float:

return self.width * self.height

class Circle:

def init(self, radius: float):

self.radius: float = radius

def area(self) -> float:

return math.pi * (self.radius**2)

def calculate_area(shape: Shape) -> float:

return shape.area()

if name == "main":

rect = Rectangle(12, 8)

rect_area = calculate_area(rect)

print(f"Rectangle area: {rect_area}")

circ = Circle(6.5)

circ_area = calculate_area(circ)

print(f"Circle area: {circ_area:.2f}")


			To see the result of this code, run the following command:

python ch02/ocp.py


			You should get the following output:

Rectangle area: 96

calculate_area 函数。新的设计优雅,并且由于遵循了 OCP,易于维护。

        因此,你现在已经发现了另一个你应该每天使用的原则,它既促进了适应不断变化的需求的设计,又保持了现有功能的不变性。

        LSP

        LSP 是面向对象编程中的另一个基本概念。它规定了子类应该如何与它们的超类相关联。根据 LSP,如果一个程序使用超类的对象,那么用子类的对象替换这些对象不应该改变程序的正确性和预期的行为。

        遵循这一原则对于保持软件系统的健壮性非常重要。它确保在使用继承时,子类在不改变其外部行为的情况下扩展其父类。例如,如果一个函数与超类对象一起工作正确,那么它也应该与这个超类的任何子类对象一起工作正确。

        LSP 允许开发者引入新的子类类型,而不会破坏现有功能的风险。这在大型系统中尤为重要,因为一个部分的更改可能会影响系统的其他部分。通过遵循 LSP,开发者可以安全地修改和扩展类,知道他们的新子类将与既定的层次结构和功能无缝集成。

        LSP 设计的一个例子

        让我们考虑一个 `Bird` 类和一个继承自它的 `Penguin` 类:
class Bird:
    def fly(self):
        print("I can fly")
class Penguin(Bird):
    def fly(self):
        print("I can't fly")
        然后,为了满足一个假设的使鸟类飞行的程序的需求,我们添加了一个 `make_bird_fly` 函数:
def make_bird_fly(bird):
    bird.fly()
        根据当前代码,我们可以看到,如果我们向函数传递 `Bird` 类的实例,我们会得到预期的行为(“鸟会飞”),而如果我们传递 `Penguin` 类的实例,我们会得到另一种行为(“它不会飞”)。你可以分析 `ch02/lsp_violation.py` 文件中提供的代表这种第一个设计代码,并运行它来测试这个结果。这至少给我们提供了 LSP 希望帮助我们避免的直觉。那么,我们如何通过遵循 LSP 来改进设计呢?

        为了遵循 LSP,我们可以重构代码并引入新的类,以确保行为保持一致:

            1.  我们保留 `Bird` 类,但使用更好的方法来表示我们想要的行为;让我们称它为 `move()`。现在这个类将看起来如下:

```py
class Bird:
    def move(self):
        print("I'm moving")
```

                1.  然后,我们引入一个 `FlyingBird` 类和一个 `FlightlessBird` 类,它们都继承自 `Bird` 类:

```py
class FlyingBird(Bird):
    def move(self):
        print("I'm flying")
class FlightlessBird(Bird):
    def move(self):
        print("I'm walking")
```

                1.  现在,`make_bird_move` 函数可以定义为以下内容:

```py
def make_bird_move(bird):
    bird.move()
```

                1.  如往常一样,我们添加一些必要的代码来测试设计:

```py
if __name__ == "__main__":
    generic_bird = Bird()
    eagle = FlyingBird()
    penguin = FlightlessBird()
    make_bird_move(generic_bird)
    make_bird_move(eagle)
    make_bird_move(penguin)
```

        这个新设计的完整代码,保存在 `ch02/lsp.py` 文件中,如下所示:
class Bird:
    def move(self):
        print("I'm moving")
class FlyingBird(Bird):
    def move(self):
        print("I'm flying")
class FlightlessBird(Bird):
    def move(self):
        print("I'm walking")
def make_bird_move(bird):
    bird.move()
if __name__ == "__main__":
    generic_bird = Bird()
    eagle = FlyingBird()
    penguin = FlightlessBird()
    make_bird_move(generic_bird)
    make_bird_move(eagle)
    make_bird_move(penguin)
        To test the example, run the following command:
python ch02/lsp.py
        You should get the following output:
I'm moving
I'm flying
Bird class with a Penguin class or with an Eagle class; that is, each object moves whether it is an instance of a Bird class or an instance of a subclass. And that result was possible thanks to following the LSP.
			This example demonstrates that all subclasses (`FlyingBird` and `FlightlessBird`) can be used in place of their superclass (`Bird`) without disrupting the expected behavior of the program. This conforms to the LSP.
			ISP
			The ISP advocates for designing smaller, more specific interfaces rather than broad, general-purpose ones. This principle states that a class should not be forced to implement interfaces it does not use. In the context of Python, this implies that a class shouldn’t be forced to inherit and implement methods that are irrelevant to its purpose.
			The ISP suggests that when designing software, one should avoid creating large, monolithic interfaces. Instead, the focus should be on creating smaller, more focused interfaces. This allows classes to only inherit or implement what they need, ensuring that each class only contains relevant and necessary methods.
			Following this principle helps us build software with modularity, code readability and maintainability qualities, reduced side effects, and software that benefits from easier refactoring and testing, among other things.
			An example of design following the ISP
			Let’s consider an `AllInOnePrinter` class that implements functionalities for printing, scanning, and faxing documents. The definition for that class would look like the following:

class AllInOnePrinter:

def print_document(self):

print("打印中")

def scan_document(self):

print("扫描中")

def fax_document(self):

print("发送传真")


			If we wanted to introduce a specialized `SimplePrinter` class that only prints, it would have to implement or inherit the `scan_document` and `fax_document` methods (even though it only prints). That is not ideal.
			To adhere to the ISP, we can create a separate interface for each functionality so that each class implements only the interfaces it needs.
			Note about interfaces
			Refer to the presentation in *Chapter 1*, *Foundational Design Principles*, of the **program to interfaces, not implementations principle**, to understand the importance of interfaces and the techniques we use in Python to define them (abstract base classes, protocols, etc.). In particular, here is the situation where protocols are the natural answer, that is, they help define small interfaces where each interface is created for doing only one thing.

				1.  Let’s start by defining the three interfaces:

    ```

    from typing import Protocol

    class Printer(Protocol):

    def print_document(self):

    ...

    class Scanner(Protocol):

    def scan_document(self):

    ...

    class Fax(Protocol):

    def fax_document(self):

    ...

    ```py

    				2.  Then, we keep the `AllInOnePrinter` class, which already implements the interfaces:

    ```

    class AllInOnePrinter:

    def print_document(self):

    print("打印中")

    def scan_document(self):

    print("扫描中")

    def fax_document(self):

    print("发送传真")

    ```py

    				3.  We add the `SimplePrinter` class, implementing the `Printer` interface, as follows:

    ```

    class SimplePrinter:

    def print_document(self):

    print("简单打印")

    ```py

    				4.  We also add a function that, when passed an object that implements the `Printer` interface, calls the right method on it to do the printing:

    ```

    def do_the_print(printer: Printer):

    printer.print_document()

    ```py

    				5.  Finally, we add code for testing the classes and the implemented interfaces:

    ```

    if __name__ == "__main__":

    all_in_one = AllInOnePrinter()

    all_in_one.scan_document()

    all_in_one.fax_document()

    do_the_print(all_in_one)

    simple = SimplePrinter()

    do_the_print(simple)

    ```py

			Here is the complete code for this new design (`ch02/isp.py`):

from typing import Protocol

class Printer(Protocol):

def print_document(self):

...

class Scanner(Protocol):

def scan_document(self):

...

class Fax(Protocol):

def fax_document(self):

...

class AllInOnePrinter:

def print_document(self):

print("打印中")

def scan_document(self):

print("扫描中")

def fax_document(self):

print("发送传真")

class SimplePrinter:

def print_document(self):

print("简单打印")

def do_the_print(printer: Printer):

printer.print_document()

if name == "main":

all_in_one = AllInOnePrinter()

all_in_one.scan_document()

all_in_one.fax_document()

do_the_print(all_in_one)

simple = SimplePrinter()

do_the_print(simple)


			To test this code, run the following command:

python ch02/isp.py


			You will get the following output:

扫描中

发送传真

Printing

简单打印


			Because of the new design, each class only needs to implement the methods relevant to its behavior. This illustrates the ISP.
			DIP
			The DIP advocates that high-level modules should not depend directly on low-level modules. Instead, both should depend on abstractions or interfaces. By doing so, you decouple the high-level components from the details of the low-level components.
			This principle allows for the reduction of the coupling between different parts of the system you are building, making it more maintainable and extendable, as we will see in an example.
			Following the DIP brings loose coupling within a system because it encourages the use of interfaces as intermediaries between different parts of the system. When high-level modules depend on interfaces, they remain isolated from the specific implementations of low-level modules. This separation of concerns enhances maintainability and extensibility.
			In essence, the DIP is closely linked to the loose coupling principle, which was covered in *Chapter 1*, *Foundational Design Principles*, by promoting a design where components interact through interfaces rather than concrete implementations. This reduces the interdependencies between modules, making it easier to modify or extend one part of the system without affecting others.
			An example of design following the ISP
			Consider a `Notification` class responsible for sending notifications via email, using an `Email` class. The code for both classes would look like the following:

class Email:

def send_email(self, message):

print(f"发送邮件: {message}")

class Notification:

def init(self):

self.email = Email()

def send(self, message):

self.email.send_email(message)


			Note about the code
			This is not yet the final version of the example.
			Currently, the high-level `Notification` class is dependent on the low-level `Email` class, and that is not ideal. To adhere to the DIP, we can introduce an abstraction, with a new code, as follows:

				1.  Define a `MessageSender` interface:

    ```

    from typing import Protocol

    class MessageSender(Protocol):

    def send(self, message: str):

    ...

    ```py

    				2.  Define the `Email` class, which implements the `MessageSender` interface, as follows:

    ```

    class Email:

    def send(self, message: str):

    print(f"发送邮件: {message}")

    ```py

    				3.  Define the `Notification` class, which also implements the `MessageSender` interface, and has an object that implements `MessageSender` stored in its `sender` attribute, for handling the actual message sending. The code for that definition is as follows:

    ```

    class Notification:

    def __init__(self, sender: MessageSender):

    self.sender: MessageSender = sender

    def send(self, message: str):

    self.sender.send(message)

    ```py

    				4.  Finally, add some code for testing the design:

    ```

    if __name__ == "__main__":

    email = Email()

    notif = Notification(sender=email)

    notif.send(message="这是消息。")

    ```py

			The complete code for the implementation we just proposed is as follows (`ch02/dip.py`):

from typing import Protocol

class MessageSender(Protocol):

def send(self, message: str):

...

class Email:

def send(self, message: str):

print(f"发送邮件: {message}")

class Notification:

def init(self, sender: MessageSender):

self.sender = sender

def send(self, message: str):

self.sender.send(message)

if name == "main":

email = Email()

notif = Notification(sender=email)

notif.send(message="这是消息。")


			To test the code, run the following command:

python ch02/dip.py


			You should get the following output:

Notification and Email are based on the MessageSender abstraction, so this design adheres to the DIP.

        Summary

        在本章中,我们探讨了比在*第一章*“基础设计原则”中介绍的原则更多的内容。理解和应用 SOLID 原则对于编写可维护、健壮和可扩展的 Python 代码至关重要。这些原则为良好的软件设计提供了坚实的基础,使得管理复杂性、减少错误和提升代码的整体质量变得更加容易。

        在下一章中,我们将开始探索 Python 中的设计模式,这是追求卓越的 Python 开发者不可或缺的一个主题。


第二部分:来自四人帮

本部分探讨了来自四人帮(GoF)的经典设计模式,这些模式用于解决日常问题,以及如何作为 Python 开发者应用它们。本部分包括以下章节:

  • 第三章“创建型设计模式”

  • 第四章“结构设计模式”

  • 第五章“行为设计模式”

第三章:创建型设计模式

设计模式是可重用的编程解决方案,已在各种实际应用场景中使用,并已被证明能产生预期的结果。它们在程序员之间共享,并且随着时间的推移而不断改进。这个主题之所以受欢迎,多亏了 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著的书籍,书名为《设计模式:可重用面向对象软件的元素》。

这里是来自“四人帮”书中关于设计模式的一句话:

设计模式系统地命名、阐述并解释了一种通用的设计,该设计解决了面向对象系统中反复出现的设计问题。它描述了问题、解决方案、何时应用解决方案及其后果。它还提供了实现提示和示例。解决方案是一组通用的对象和类安排,用于解决问题。解决方案是根据特定环境进行定制和实现的,以解决该问题。

根据它们解决的问题类型和/或它们帮助我们构建的解决方案类型,面向对象编程OOP)中使用了几个设计模式类别。在他们的书中,“四人帮”提出了 23 个设计模式,分为三个类别:创建型结构型行为型

__init__() 函数,不方便。

在本章中,我们将涵盖以下主要内容:

  • 工厂模式

  • 构建者模式

  • 原型模式

  • 单例模式

  • 对象池模式

到本章结束时,你将有一个稳固的理解,无论是它们在 Python 中是否有用,以及如何在它们有用时使用它们。

技术要求

请参阅第一章中提出的需求。

工厂模式

我们将从“四人帮”书中的第一个创建型设计模式开始:工厂设计模式。在工厂设计模式中,客户端(即客户端代码)请求一个对象,而不知道这个对象是从哪里来的(即,使用哪个类来生成它)。工厂背后的想法是简化对象创建过程。如果通过一个中心函数来完成,那么跟踪哪些对象被创建比让客户端使用直接类实例化来创建对象要容易得多。工厂通过将创建对象的代码与使用它的代码解耦,减少了维护应用程序的复杂性。

工厂通常有两种形式——工厂方法,它是一个方法(对于 Python 开发者来说,也可以是一个简单的函数),根据输入参数返回不同的对象,以及抽象工厂,它是一组用于创建相关对象族的工厂方法。

让我们讨论两种工厂模式的形式,从工厂方法开始。

工厂方法

工厂方法基于一个编写来处理我们的对象创建任务的单一函数。我们执行它,传递一个参数,提供关于我们想要什么的信息,然后,作为结果,所需的对象被创建。

有趣的是,在使用工厂方法时,我们不需要了解任何关于结果对象是如何实现以及从哪里来的细节。

现实世界中的例子

我们可以在塑料玩具构建套件的背景下找到工厂方法模式在现实生活中的应用。用于构建塑料玩具的模具材料是相同的,但通过使用正确的塑料模具,可以生产出不同的玩具(不同的形状或图案)。这就像有一个工厂方法,其中输入是我们想要的玩具名称(例如,鸭子或汽车),输出(成型后)是我们请求的塑料玩具。

在软件世界中,Django Web 框架使用工厂方法模式来创建网页表单的字段。Django 包含的 forms 模块(github.com/django/django/blob/main/django/forms/forms.py)支持创建不同类型的字段(例如,CharFieldEmailField 等)。它们的行为的一部分可以通过 max_lengthrequired 等属性进行自定义。

工厂方法模式的用例

如果你意识到你无法追踪应用程序创建的对象,因为创建它们的代码分布在许多不同的地方而不是一个单独的函数/方法中,你应该考虑使用工厂方法模式。工厂方法集中了对象的创建,跟踪对象变得容易得多。请注意,创建多个工厂方法是完全可以接受的,这也是实践中通常的做法。每个工厂方法逻辑上分组创建具有相似性的对象。例如,一个工厂方法可能负责连接到不同的数据库(MySQL、SQLite);另一个工厂方法可能负责创建你请求的几何对象(圆形、三角形);等等。

当你想要将对象创建与对象使用解耦时,工厂方法也非常有用。在创建对象时,我们并不依赖于特定的类;我们只是通过调用一个函数来提供关于我们想要什么的部分信息。这意味着修改函数是容易的,并且不需要对其使用的代码进行任何更改。

另一个值得提及的使用案例与提高应用程序的性能和内存使用有关。工厂方法可以通过仅在必要时创建新对象来提高性能和内存使用。当我们使用直接类实例化创建对象时,每次创建新对象时都会分配额外的内存(除非类内部使用缓存,这通常不是情况)。我们可以在以下代码(ch03/factory/id.py)中看到这一点,该代码创建了MyClass类的两个实例,并使用id()函数比较它们的内存地址。地址也打印在输出中,以便我们可以检查它们。内存地址不同的事实意味着创建了两个不同的对象。代码如下:

class MyClass:
    pass
if __name__ == "__main__":
    a = MyClass()
    b = MyClass()
    print(id(a) == id(b))
    print(id(a))
    print(id(b))

在我的计算机上执行代码(ch03/factory/id.py)的结果如下:

False
4330224656
4331646704

注意

当你执行文件时看到的地址,其中调用了id()函数,与我看到的地址不同,因为它们依赖于当前的内存布局和分配。但结果必须相同——这两个地址应该是不同的。有一个例外,如果你在 Python 读-求值-打印循环REPL)中编写和执行代码——或者简单地说,交互式提示符——那么那是一个 REPL 特定的优化,通常不会发生。

实现工厂方法模式

数据以多种形式存在。存储/检索数据主要有两种文件类别:可读文件和二进制文件。可读文件的例子有 XML、RSS/Atom、YAML 和 JSON。二进制文件的例子有 SQLite 使用的.sq3文件格式和用于听音乐的.mp3音频文件格式。

在这个例子中,我们将关注两种流行的可读格式——XML 和 JSON。尽管可读文件通常比二进制文件解析速度慢,但它们使数据交换、检查和修改变得容易得多。因此,建议你在没有其他限制不允许的情况下(主要是不可接受的性能或专有二进制格式)与可读文件一起工作。

在这种情况下,我们有一些输入数据存储在 XML 和 JSON 文件中,我们想要解析它们并检索一些信息。同时,我们想要集中管理客户端对这些(以及所有未来的)外部服务的连接。我们将使用工厂方法来解决这个问题。示例仅关注 XML 和 JSON,但添加对更多服务的支持应该是简单的。

首先,让我们看看数据文件。

JSON 文件,movies.json,是一个包含关于美国电影(标题、年份、导演姓名、类型等)信息的示例数据集:

[
  {
    "title": "After Dark in Central Park",
    "year": 1900,
    "director": null,
    "cast": null,
    "genre": null
  },
  {
    "title": "Boarding School Girls' Pajama Parade",
    "year": 1900,
    "director": null,
    "cast": null,
    "genre": null
  },
  {
    "title": "Buffalo Bill's Wild West Parad",
    "year": 1900,
    "director": null,
    "cast": null,
    "genre": null
  },
  {
    "title": "Caught",
    "year": 1900,
    "director": null,
    "cast": null,
    "genre": null
  },
  {
    "title": "Clowns Spinning Hats",
    "year": 1900,
    "director": null,
    "cast": null,
    "genre": null
  },
  {
    "title": "Capture of Boer Battery by British",
    "year": 1900,
    "director": "James H. White",
    "cast": null,
    "genre": "Short documentary"
  },
  {
    "title": "The Enchanted Drawing",
    "year": 1900,
    "director": "J. Stuart Blackton",
    "cast": null,
    "genre": null
  },
  {
    "title": "Family Troubles",
    "year": 1900,
    "director": null,
    "cast": null,
    "genre": null
  },
  {
    "title": "Feeding Sea Lions",
    "year": 1900,
    "director": null,
    "cast": "Paul Boyton",
    "genre": null
  }
]

XML 文件,person.xml,包含有关个人(firstNamelastNamegender等)的信息,如下所示:

  1. 我们从persons XML 容器的封装标签开始:

    <persons>
    
  2. 然后,一个表示个人数据代码的 XML 元素如下所示:

    <person>
      <firstName>John</firstName>
      <lastName>Smith</lastName>
      <age>25</age>
      <address>
        <streetAddress>21 2nd Street</streetAddress>
        <city>New York</city>
        <state>NY</state>
        <postalCode>10021</postalCode>
      </address>
      <phoneNumbers>
        <number type="home">212 555-1234</number>
        <number type="fax">646 555-4567</number>
      </phoneNumbers>
      <gender>
        <type>male</type>
      </gender>
    </person>
    
  3. 一个表示另一个人数据的 XML 元素如下所示:

    <person>
      <firstName>Jimy</firstName>
      <lastName>Liar</lastName>
      <age>19</age>
      <address>
        <streetAddress>18 2nd Street</streetAddress>
        <city>New York</city>
        <state>NY</state>
        <postalCode>10021</postalCode>
      </address>
      <phoneNumbers>
        <number type="home">212 555-1234</number>
      </phoneNumbers>
      <gender>
        <type>male</type>
      </gender>
    </person>
    
  4. 一个表示第三个人数据的 XML 元素如下所示:

    <person>
      <firstName>Patty</firstName>
      <lastName>Liar</lastName>
      <age>20</age>
      <address>
        <streetAddress>18 2nd Street</streetAddress>
        <city>New York</city>
        <state>NY</state>
        <postalCode>10021</postalCode>
      </address>
      <phoneNumbers>
        <number type="home">212 555-1234</number>
        <number type="mobile">001 452-8819</number>
      </phoneNumbers>
      <gender>
        <type>female</type>
      </gender>
    </person>
    
  5. 最后,我们关闭 XML 容器:

    </persons>
    

我们将使用 Python 发行版中用于处理 JSON 和 XML 的两个库:jsonxml.etree.ElementTree

我们首先导入所需的模块以进行各种操作(jsonElementTreepathlib),并定义一个JSONDataExtractor类,从文件中加载数据,并使用parsed_data属性来获取它。这部分代码如下:

import json
import xml.etree.ElementTree as ET
from pathlib import Path
class JSONDataExtractor:
    def __init__(self, filepath: Path):
        self.data = {}
        with open(filepath) as f:
            self.data = json.load(f)
    @property
    def parsed_data(self):
        return self.data

我们还定义了一个XMLDataExtractor类,通过ElementTree的解析器加载文件中的数据,并使用parsed_data属性来获取结果,如下所示:

class XMLDataExtractor:
    def __init__(self, filepath: Path):
        self.tree = ET.parse(filepath)
    @property
    def parsed_data(self):
        return self.tree

现在,我们提供工厂函数,该函数根据目标文件的扩展名选择正确的数据提取器类(如果不受支持,则抛出异常),如下所示:

def extract_factory(filepath: Path):
    ext = filepath.name.split(".")[-1]
    if ext == "json":
        return JSONDataExtractor(filepath)
    elif ext == "xml":
        return XMLDataExtractor(filepath)
    else:
        raise ValueError("Cannot extract data")

接下来,我们定义我们程序的主要函数,extract();在函数的第一部分,代码处理 JSON 情况,如下所示:

def extract(case: str):
    dir_path = Path(__file__).parent
    if case == "json":
        path = dir_path / Path("movies.json")
        factory = extract_factory(path)
        data = factory.parsed_data
        for movie in data:
            print(f"- {movie['title']}")
            director = movie["director"]
            if director:
                print(f"   Director: {director}")
            genre = movie["genre"]
            if genre:
                print(f"   Genre: {genre}")

我们添加了extract()函数的最后部分,使用工厂方法处理 XML 文件。使用 XPath 查找所有姓氏为Liar的个人元素。对于每个匹配的个人,显示基本姓名和电话号码信息。代码如下:

    elif case == "xml":
        path = dir_path / Path("person.xml")
        factory = extract_factory(path)
        data = factory.parsed_data
        search_xpath = ".//person[lastName='Liar']"
        items = data.findall(search_xpath)
        for item in items:
            first = item.find("firstName").text
            last = item.find("lastName").text
            print(f"- {first} {last}")
            for pn in item.find("phoneNumbers"):
                pn_type = pn.attrib["type"]
                pn_val = pn.text
                phone = f"{pn_type}: {pn_val}"
                print(f"   {phone}")

最后,我们添加一些测试代码:

if __name__ == "__main__":
    print("* JSON case *")
    extract(case="json")
    print("* XML case *")
    extract(case="xml")

这里是实现总结(在ch03/factory/factory_method.py文件中):

  1. 在导入所需的模块后,我们首先定义一个 JSON 数据提取类(JSONDataExtractor)和一个 XML 数据提取类(XMLDataExtractor)。

  2. 我们添加了一个工厂函数,extract_factory(),以获取正确的数据提取器类进行实例化。

  3. 我们还添加了我们的包装器和主函数,extract()

  4. 最后,我们添加测试代码,从 JSON 文件和 XML 文件中提取数据并解析结果文本。

要测试示例,请运行以下命令:

python ch03/factory/factory_method.py

你应该得到以下输出:

* JSON case *
- After Dark in Central Park
- Boarding School Girls' Pajama Parade
- Buffalo Bill's Wild West Parad
- Caught
- Clowns Spinning Hats
- Capture of Boer Battery by British
   Director: James H. White
   Genre: Short documentary
- The Enchanted Drawing
   Director: J. Stuart Blackton
- Family Troubles
- Feeding Sea Lions
* XML case *
- Jimy Liar
   home: 212 555-1234
- Patty Liar
   home: 212 555-1234
   mobile: 001 452-8819

注意,尽管JSONDataExtractorXMLDataExtractor具有相同的接口,但parsed_data()返回的内容处理方式并不统一;在一种情况下我们有一个列表,在另一种情况下我们有一个树。必须使用不同的 Python 代码来处理每个数据提取器。虽然能够为所有提取器使用相同的代码会很理想,但在大多数情况下这是不现实的,除非我们使用某种类型的数据通用映射,这通常由外部数据提供者提供。假设你可以使用相同的代码来处理 XML 和 JSON 文件,那么为了支持第三种格式——例如 SQLite,需要进行哪些更改?找到一个 SQLite 文件或创建自己的文件并尝试它。

你应该使用工厂方法模式吗?

经验丰富的 Python 开发者经常对工厂方法模式提出的主要批评是,它对于许多用例来说可能被认为是过度设计或过于复杂。Python 的动态类型和一等函数通常允许对工厂方法旨在解决的问题有更简单、更直接的方法。在 Python 中,你通常可以直接使用简单函数或类方法来创建对象,而无需创建单独的工厂类或函数。这使代码更具可读性和 Python 风格,遵循语言“简单比复杂好”的哲学。

此外,Python 对默认参数、关键字参数和其他语言特性的支持通常使得向后兼容地扩展构造函数变得更加容易,从而减少了单独工厂方法的需求。因此,虽然工厂方法模式是在静态类型语言(如 Java 或 C++)中建立起来的一个良好的设计模式,但它通常被认为对于 Python 更灵活和动态的本质来说过于繁琐或冗长。

为了展示在没有工厂方法模式的情况下如何处理简单用例,已在 ch03/factory/factory_method_not_needed.py 文件中提供了一个替代实现。正如你所看到的,不再有工厂。以下代码摘录显示了当我们说在 Python 中,你只需在需要的地方创建对象,而不需要一个中间函数或类,这使得你的代码更具 Python 风格的含义:

if case == "json":
    path = dir_path / Path("movies.json")
    data = JSONDataExtractor(path).parsed_data

抽象工厂模式

抽象工厂模式是工厂方法思想的泛化。基本上,抽象工厂是一组(逻辑)工厂方法,其中每个工厂方法负责生成不同类型的对象。

我们将讨论一些示例、用例和可能的实现。

现实世界示例

抽象工厂在汽车制造中得到了应用。相同的机器用于不同车型(车门、面板、引擎盖、挡泥板和镜子)的部件冲压。由机器组装的模型是可配置的,并且可以随时更改。

在软件类别中,factory_boy 包(github.com/FactoryBoy/factory_boy)为测试中创建 Django 模型提供了一个抽象工厂实现。另一个工具是 model_bakery (github.com/model-bakers/model_bakery)。这两个包都用于创建支持特定测试属性的模式实例。这很重要,因为这样,可以提高测试的可读性,并避免共享不必要的代码。

注意

Django 模型是框架用来帮助存储和与数据库(表)中的数据交互的特殊类。有关更多详细信息,请参阅 Django 文档(docs.djangoproject.com)。

抽象工厂模式的使用案例

由于抽象工厂模式是工厂方法模式的泛化,它提供了相同的优点:它使跟踪对象创建变得更容易,它将对象创建与对象使用解耦,并且它为我们提供了改进应用程序内存使用和性能的潜力。

实现抽象工厂模式

为了演示抽象工厂模式,我将重用我最喜欢的例子之一,它包含在 Bruce Eckel 所著的《Python 3 Patterns, Recipes and Idioms》一书中。想象一下,我们正在创建一个游戏,或者我们想在应用程序中包含一个迷你游戏来娱乐用户。我们希望包含至少两个游戏,一个供儿童玩,一个供成人玩。我们将根据用户输入在运行时决定创建和启动哪个游戏。抽象工厂负责游戏创建部分。

让我们从儿童游戏开始。它被称为interact_with()方法,用于描述青蛙与障碍(例如,虫子、谜题和其他青蛙)的交互,如下所示:

class Frog:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return self.name
    def interact_with(self, obstacle):
        act = obstacle.action()
        msg = f"{self} the Frog encounters {obstacle} and {act}!"
        print(msg)

可能会有许多种障碍,但就我们的例子而言,障碍只能是一个虫子。当青蛙遇到虫子时,只支持一个动作。它会吃掉它:

class Bug:
    def __str__(self):
        return "a bug"
    def action(self):
        return "eats it"

FrogWorld类是一个抽象工厂。其主要职责是创建游戏中的主要角色和障碍。将创建方法分开并使用通用的名称(例如,make_character()make_obstacle())允许我们动态地更改活动工厂(因此,活动游戏)而无需任何代码更改。代码如下:

class FrogWorld:
    def __init__(self, name):
        print(self)
        self.player_name = name
    def __str__(self):
        return "\n\n\t------ Frog World -------"
    def make_character(self):
        return Frog(self.player_name)
    def make_obstacle(self):
        return Bug()

WizardWorld游戏类似。唯一的区别是法师与食虫虫的怪物如兽人战斗,而不是吃虫子!

这里是Wizard类的定义,它与Frog类类似:

class Wizard:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return self.name
    def interact_with(self, obstacle):
        act = obstacle.action()
        msg = f"{self} the Wizard battles against {obstacle} and {act}!"
        print(msg)

然后,Ork类的定义如下:

class Ork:
    def __str__(self):
        return "an evil ork"
    def action(self):
        return "kills it"

我们还需要定义一个WizardWorld类,类似于我们讨论过的FrogWorld类;在这种情况下,障碍是一个Ork实例:

class WizardWorld:
    def __init__(self, name):
        print(self)
        self.player_name = name
    def __str__(self):
        return "\n\n\t------ Wizard World -------"
    def make_character(self):
        return Wizard(self.player_name)
    def make_obstacle(self):
        return Ork()

GameEnvironment类是我们游戏的主要入口点。它接受工厂作为输入,并使用它来创建游戏的世界。play()方法启动创建的英雄与障碍之间的交互,如下所示:

class GameEnvironment:
    def __init__(self, factory):
        self.hero = factory.make_character()
        self.obstacle = factory.make_obstacle()
    def play(self):
        self.hero.interact_with(self.obstacle)

validate_age()函数提示用户输入有效的年龄。如果年龄无效,它返回一个元组,第一个元素设置为False。如果年龄有效,元组的第一个元素设置为True,这就是我们关注元组的第二个元素的情况,即用户输入的年龄,如下所示:

def validate_age(name):
    age = None
    try:
        age_input = input(
            f"Welcome {name}. How old are you? "
        )
        age = int(age_input)
    except ValueError:
        print(
            f"Age {age} is invalid, please try again..."
        )
        return False, age
    return True, age

最后是main()函数的定义,然后调用它。它询问用户的姓名和年龄,并根据用户的年龄决定应该玩哪个游戏,如下所示:

def main():
    name = input("Hello. What's your name? ")
    valid_input = False
    while not valid_input:
        valid_input, age = validate_age(name)
    game = FrogWorld if age < 18 else WizardWorld
    environment = GameEnvironment(game(name))
    environment.play()
if __name__ == "__main__":
    main()

我们刚才讨论的实现总结(请参阅ch03/factory/abstract_factory.py文件中的完整代码)如下:

  1. 我们为FrogWorld游戏定义了FrogBug类。

  2. 我们添加了一个FrogWorld类,其中我们使用了我们的FrogBug类。

  3. 我们为WizardWorld游戏定义了WizardOrk类。

  4. 我们添加了一个WizardWorld类,其中我们使用了我们的WizardOrk类。

  5. 我们定义了一个GameEnvironment类。

  6. 我们添加了一个validate_age()函数。

  7. 最后,我们有main()函数,接着是调用它的传统技巧。以下是这个函数的几个方面:

    • 我们获取用户的姓名和年龄输入。

    • 我们根据用户的年龄决定使用哪个游戏类。

    • 我们实例化正确的游戏类,然后是GameEnvironment类。

    • 我们在environment对象上调用.play()来玩游戏。

让我们使用python ch03/factory/abstract_factory.py命令调用这个程序,并查看一些示例输出。

青少年的示例输出如下:

Hello. What's your name? Arthur
Welcome Arthur. How old are you? 13
------ Frog World -------
Arthur the Frog encounters a bug and eats it!

成人的示例输出如下:

Hello. What's your name? Tom
Welcome Tom. How old are you? 34
------ Wizard World -------
Tom the Wizard battles against an evil ork and kills it!

尝试扩展游戏使其更加完整。你可以做到你想做的程度;创建许多障碍、许多敌人,以及你喜欢的任何其他东西。

构建器模式

我们刚刚介绍了前两种创建型模式,即工厂方法和抽象工厂方法,它们都提供了在非平凡情况下改进我们创建对象的方法。

现在,假设我们想要创建一个由多个部分组成的对象,并且组合需要逐步完成。除非所有部分都完全创建,否则对象不是完整的。这就是构建器设计模式能帮助我们的地方。构建器设计模式将复杂对象的构建与其表示分离。通过将构建与表示分离,相同的构建可以用来创建几个不同的表示。

现实世界示例

在我们的日常生活中,构建器设计模式在快餐店中被使用。制作汉堡和包装(盒子和平装袋)的相同程序总是被使用,即使有各种各样的汉堡(经典汉堡、芝士汉堡等等)和不同的包装(小号盒子、中号盒子等等)。经典汉堡和芝士汉堡之间的区别在于表示,而不是构建过程。在这种情况下,导演是收银员,他向工作人员下达需要准备的指令,而构建者是负责特定订单的工作人员。

软件中,我们可以考虑django-query-builder库(github.com/ambitioninc/django-query-builder),这是一个依赖构建器模式的第三方 Django 库。这个库可以用来动态构建 SQL 查询,允许你控制查询的所有方面,并创建从简单到非常复杂的各种查询。

与工厂模式的比较

到这一点,构建器模式和工厂模式之间的区别可能不是很清楚。主要区别是,工厂模式在单步中创建对象,而构建器模式在多步中创建对象,并且几乎总是使用一个导演

另一个区别是,虽然工厂模式立即返回创建的对象,但在构建器模式中,客户端代码明确要求导演在需要时返回最终对象。

构建器模式的用例

当一个对象需要用许多可能的配置构建时,构建器模式特别有用。一个典型的情况是,一个类有多个构造函数,参数数量不同,这往往会导致混淆或容易出错的代码。

当对象的构建过程比简单地设置初始值更复杂时,该模式也有益。例如,如果一个对象的完整创建涉及多个步骤,如参数验证、设置数据结构或甚至调用外部服务,构建器模式可以封装这种复杂性。

实现构建器模式

让我们看看我们如何使用构建器设计模式来制作一个点餐应用程序。这个例子特别有趣,因为披萨的制备需要遵循特定的顺序。要加酱料,你首先需要准备面团。要加配料,你首先需要加酱料。除非酱料和配料都放在面团上,否则你不能开始烤披萨。此外,每块披萨通常需要不同的烘烤时间,这取决于面团的厚度和使用的配料。

我们首先导入所需的模块,并声明一些Enum参数以及一个在应用程序中多次使用的常量。STEP_DELAY常量用于在准备披萨的不同步骤之间添加时间延迟,如下所示:

import time
from enum import Enum
PizzaProgress = Enum(
    "PizzaProgress", "queued preparation baking ready"
)
PizzaDough = Enum("PizzaDough", "thin thick")
PizzaSauce = Enum("PizzaSauce", "tomato creme_fraiche")
PizzaTopping = Enum(
    "PizzaTopping",
    "mozzarella double_mozzarella bacon ham mushrooms red_onion oregano",
)
# Delay in seconds
STEP_DELAY = 3

我们最终的产品是披萨,由Pizza类描述。当使用构建器模式时,最终产品没有很多责任,因为它不应该直接实例化。构建器创建最终产品的实例,并确保它被正确准备。这就是为什么Pizza类如此极简。它基本上将所有数据初始化为合理的默认值。一个例外是prepare_dough()方法。

prepare_dough()方法定义在Pizza类中而不是构建器中,有两个原因。首先,为了阐明最终产品通常是极简的,这并不意味着你永远不应该给它分配任何责任。其次,为了通过组合来促进代码重用。

因此,我们定义我们的Pizza类如下:

class Pizza:
    def __init__(self, name):
        self.name = name
        self.dough = None
        self.sauce = None
        self.topping = []
    def __str__(self):
        return self.name
    def prepare_dough(self, dough):
        self.dough = dough
        print(
            f"preparing the {self.dough.name} dough of your {self}..."
        )
        time.sleep(STEP_DELAY)
        print(f"done with the {self.dough.name} dough")

有两个构建器:一个用于创建玛格丽塔披萨(MargaritaBuilder)和另一个用于创建奶油培根披萨(CreamyBaconBuilder)。每个构建器创建一个 Pizza 实例,并包含遵循披萨制作程序的各个方法:prepare_dough()add_sauce()add_topping()bake()。更准确地说,prepare_dough() 只是 Pizza 类中 prepare_dough() 方法的包装。

注意每个构建器如何处理所有与披萨相关的细节。例如,玛格丽塔披萨的配料是双份马苏里拉奶酪和牛至,而奶油培根披萨的配料是马苏里拉奶酪、培根、火腿、蘑菇、红洋葱和牛至。

MargaritaBuilder 类的代码片段如下(完整的代码请参阅 ch03/builder.py 文件):

class MargaritaBuilder:
    def __init__(self):
        self.pizza = Pizza("margarita")
        self.progress = PizzaProgress.queued
        self.baking_time = 5
    def prepare_dough(self):
        self.progress = PizzaProgress.preparation
        self.pizza.prepare_dough(PizzaDough.thin)
    ...

CreamyBaconBuilder 类的代码片段如下:

class CreamyBaconBuilder:
    def __init__(self):
        self.pizza = Pizza("creamy bacon")
        self.progress = PizzaProgress.queued
        self.baking_time = 7
    def prepare_dough(self):
        self.progress = PizzaProgress.preparation
        self.pizza.prepare_dough(PizzaDough.thick)
    ...

在此示例中,导演 是服务员。Waiter 类的核心是 construct_pizza() 方法,它接受一个构建器作为参数并按正确顺序执行所有披萨准备步骤。选择合适的构建器,甚至可以在运行时完成,这使我们能够创建不同的披萨风格,而无需修改导演(Waiter)的任何代码。Waiter 类还包含 pizza() 方法,该方法将最终产品(准备好的披萨)作为变量返回给调用者。该类的代码如下:

class Waiter:
    def __init__(self):
        self.builder = None
    def construct_pizza(self, builder):
        self.builder = builder
        steps = (
            builder.prepare_dough,
            builder.add_sauce,
            builder.add_topping,
            builder.bake,
        )
        [step() for step in steps]
    @property
    def pizza(self):
        return self.builder.pizza

validate_style() 方法与本章前面标题为 工厂模式 的部分中描述的 validate_age() 函数类似。它用于确保用户输入有效,在这种情况下是一个映射到披萨构建器的字符。m 字符使用 MargaritaBuilder 类,而 c 字符使用 CreamyBaconBuilder 类。这些映射在 builder 参数中。返回一个元组,第一个元素设置为 True 如果输入有效或 False 如果无效,如下所示:

def validate_style(builders):
    try:
        input_msg = "What pizza would you like, [m]argarita or [c]reamy bacon? "
        pizza_style = input(input_msg)
        builder = builders[pizza_style]()
        valid_input = True
    except KeyError:
        error_msg = "Sorry, only margarita (key m) and creamy bacon (key c) are available"
        print(error_msg)
        return (False, None)
    return (True, builder)

最后的部分是 main() 函数。main() 函数包含实例化披萨构建器的代码。然后,Waiter 导演使用披萨构建器准备披萨。创建的披萨可以在任何后续时间点交付给客户:

def main():
    builders = dict(m=MargaritaBuilder, c=CreamyBaconBuilder)
    valid_input = False
    while not valid_input:
        valid_input, builder = validate_style(builders)
    print()
    waiter = Waiter()
    waiter.construct_pizza(builder)
    pizza = waiter.pizza
    print()
    print(f"Enjoy your {pizza}!")

这里是实现总结(在 ch03/builder.py 文件中):

  1. 我们开始于需要的一些导入,对于标准的 Enum 类和 time 模块。

  2. 我们声明了一些常量的变量:PizzaProgressPizzaDoughPizzaSaucePizzaToppingSTEP_DELAY

  3. 我们定义了我们的 Pizza 类。

  4. 我们为两个构建器定义了类,MargaritaBuilderCreamyBaconBuilder

  5. 我们定义了我们的 Waiter 类。

  6. 我们添加了一个 validate_style() 函数来改进异常处理。

  7. 最后,我们有 main() 函数,随后是程序运行时调用它的代码片段。在 main() 函数中,以下操作发生:

    • 我们通过validate_style()函数进行验证后,使它能够根据用户的输入选择披萨构建器。

    • 服务员使用披萨构建器来准备披萨。

    • 然后将制作的披萨送出。

下面是调用python ch03/builder.py命令执行此示例程序产生的输出:

What pizza would you like, [m]argarita or [c]reamy bacon? c
preparing the thick dough of your creamy bacon...
done with the thick dough
adding the crème fraîche sauce to your creamy bacon
done with the crème fraîche sauce
adding the topping (mozzarella, bacon, ham, mushrooms, red onion, oregano) to your creamy bacon
done with the topping (mozzarella, bacon, ham, mushrooms, red onion, oregano)
baking your creamy bacon for 7 seconds
your creamy bacon is ready
Enjoy your creamy bacon!

这是一个很好的结果。

但是...只支持两种披萨类型是件遗憾的事。想要一个夏威夷披萨构建器吗?在考虑了优势和劣势之后,考虑使用继承。或者组合,正如我们在第一章中看到的,它有其优势。

原型模式

原型模式允许您通过复制现有对象来创建新对象,而不是从头开始创建。当初始化对象的成本比复制现有对象更昂贵或更复杂时,此模式特别有用。本质上,原型模式通过复制现有实例来创建类的新的实例,从而避免了初始化新对象的额外开销。

在其最简单的版本中,这个模式只是一个接受对象作为输入参数并返回其副本的clone()函数。在 Python 中,可以使用copy.deepcopy()函数来实现这一点。

真实世界的例子

通过剪枝繁殖植物是原型模式的一个真实世界的例子。使用这种方法,你不是从种子中生长植物;而是创建一个新的植物,它是现有植物的副本。

许多 Python 应用程序都使用原型模式,但很少将其称为原型,因为克隆对象是 Python 语言的一个内置功能。

原型模式的用例

当我们有一个需要保持不变且我们想要创建其精确副本的现有对象时,原型模式非常有用,允许在副本的某些部分进行更改。

除了从数据库中复制并具有对其他基于数据库的对象引用的对象外,还需要频繁地复制对象。克隆这样一个复杂的对象成本很高(对数据库进行多次查询),因此原型是一个方便解决问题的方法。

实现原型模式

现在,一些组织,甚至规模较小的组织,通过其基础设施/DevOps 团队、托管提供商或云服务提供商CSPs)处理许多网站和应用。

当你必须管理多个网站时,有一个点变得难以跟踪。你需要快速访问信息,比如涉及的 IP 地址、域名及其到期日期,以及 DNS 参数的详细信息。因此,你需要一种库存工具。

让我们想象一下这些团队如何处理这种类型的数据以进行日常活动,并简要讨论实现一个帮助整合和维护数据的软件(除了 Excel 表格之外)。

首先,我们需要导入 Python 的标准copy模块,如下所示:

import copy

在这个系统的核心,我们将有一个Website类来存储所有有用的信息,例如名称、域名、描述、我们管理的网站的作者等。

在类的__init__()方法中,只有一些参数是固定的:namedomaindescription。但我们还希望有灵活性,客户端代码可以使用kwargs变量长度集合(每个对成为kwargs Python 字典的一项)以name=value的形式传递更多参数。

其他信息

Python 有一个帮助在obj对象上设置任意属性名为attr、值为val的惯用语,使用内置的setattr()函数:setattr(obj, attr, val)

因此,我们定义了一个Website类并初始化其对象,使用setattr技术为可选属性,如下所示:

class Website:
    def __init__(
        self,
        name: str,
        domain: str,
        description: str,
        **kwargs,
    ):
        self.name = name
        self.domain = domain
        self.description = description
        for key in kwargs:
            setattr(self, key, kwargs[key])

这还不算完。为了提高类的可用性,我们还添加了其字符串表示方法(__str__())。我们使用vars()技巧提取所有实例属性的值,并将这些值注入方法返回的字符串中。此外,由于我们计划克隆对象,我们还使用id()函数包含对象的内存地址。代码如下:

def __str__(self) -> str:
    summary = [
        f"- {self.name} (ID: {id(self)})\n",
    ]
    infos = vars(self).items()
    ordered_infos = sorted(infos)
    for attr, val in ordered_infos:
        if attr == "name":
            continue
        summary.append(f"{attr}: {val}\n")
    return "".join(summary)

其他信息

Python 中的vars()函数返回对象的__dict__属性。__dict__属性是一个包含对象属性(数据属性和方法)的字典。这个函数对于调试很有用,因为它允许你检查对象或函数内的局部变量的属性和方法。但请注意,并非所有对象都有__dict__属性。例如,列表和字典等内置类型没有这个属性。

接下来,我们添加一个实现原型设计模式的Prototype类。在这个类的核心,我们有clone()方法,它负责使用copy.deepcopy()函数克隆对象。

注意

当我们使用copy.deepcopy()克隆对象时,克隆对象的内存地址必须与原始对象的内存地址不同。

由于克隆意味着我们允许为可选属性设置值,请注意我们在这里如何使用setattr技术与attrs字典。此外,为了方便起见,Prototype类包含register()unregister()方法,这些方法可以用来跟踪注册表(字典)中的克隆对象。该类的代码如下:

class Prototype:
    def __init__(self):
        self.registry = {}
    def register(self, identifier: int, obj: object):
        self.registry[identifier] = obj
    def unregister(self, identifier: int):
        del self.registry[identifier]
    def clone(self, identifier: int, **attrs) -> object:
        found = self.registry.get(identifier)
        if not found:
            raise ValueError(
              f"Incorrect object identifier: {identifier}"
            )
        obj = copy.deepcopy(found)
        for key in attrs:
            setattr(obj, key, attrs[key])
        return obj

在我们接下来定义的main()函数中,我们完成程序:我们克隆一个Website实例,命名为site1,以获取第二个对象site2。基本上,我们实例化Prototype类,并使用其.clone()方法。然后,我们显示结果。该函数的代码如下:

def main():
    keywords = (
        "python",
        "programming",
        "scripting",
        "data",
        "automation",
    )
    site1 = Website(
        "Python",
        domain="python.org",
        description="Programming language and ecosystem",
        category="Open Source Software",
        keywords=keywords,
    )
    proto = Prototype()
    proto.register("python-001", site1)
    site2 = proto.clone(
        "python-001",
        name="Python Package Index",
        domain="pypi.org",
        description="Repository for published packages",
        category="Open Source Software",
    )
    for site in (site1, site2):
        print(site)

最后,我们调用main()函数,如下所示:

if __name__ == "__main__":
    main()

这里是对我们在代码中执行的操作的总结(ch03/prototype.py):

  1. 我们首先导入copy模块。

  2. 我们定义了一个Website类,它具有初始化方法(__init__())和字符串表示方法(__str__())。

  3. 我们定义了前面展示的Prototype类。

  4. 然后,我们有main()函数,其中我们执行以下操作:

    • 我们定义了我们需要的keywords列表。

    • 我们创建Website类的实例,称为site1(这里我们使用keywords列表)。

    • 我们创建一个Prototype对象,并使用其register()方法将site1及其标识符注册(这有助于我们跟踪字典中的克隆对象)。

    • 我们克隆site1对象以获得site2

    • 我们显示结果(两个Website对象)。

当我在电脑上执行python ch03/prototype.py命令时的一个示例输出如下:

- Python (ID: 4369628560)
category: Open Source Software
description: Programming language and ecosystem
domain: python.org
keywords: ('python', 'programming', 'scripting', 'data', 'automation')
- Python Package Index (ID: 4369627552)
category: Open Source Software
description: Repository site for Python's published packages
domain: pypi.org
keywords: ('python', 'programming', 'scripting', 'data', 'automation')

事实上,Prototype按预期工作。我们可以看到原始Website对象及其克隆的信息。

通过查看每个Website对象的 ID 值,我们可以看到两个地址是不同的。

单例模式

单例模式是面向对象编程的一个原始设计模式,它限制了一个类的实例化只能有一个对象,这在需要单个对象来协调系统动作时非常有用。

基本思想是,为了满足程序的需求,只为特定类创建一个执行特定工作的实例。为了确保这一点,我们需要防止类被多次实例化和克隆的机制。

在 Python 程序员社区中,单例模式实际上被认为是一种反模式。让我们首先探讨这个模式,然后我们将讨论我们被鼓励在 Python 中使用的替代方法。

现实世界示例

在现实生活中,我们可以想到一艘船或船的船长。在船上,他们是负责人。他们负责重要的决策,并且由于这个责任,许多请求都指向他们。

另一个例子是办公室环境中的打印机打印队列,它确保打印作业通过一个单一点协调,避免冲突并确保有序打印。

单例模式的用例

单例设计模式在你需要创建单个对象或需要某种能够维护程序全局状态的对象时非常有用。

其他可能的用例如下:

  • 控制对共享资源的并发访问——例如,管理数据库连接的类

  • 一种跨越应用程序不同部分或不同用户访问的服务或资源,并执行其工作——例如,日志系统或实用程序的核心类

实现单例模式

如前所述,单例模式确保一个类只有一个实例,并提供了一个全局点来访问它。在这个例子中,我们将创建一个 URLFetcher 类,用于从网页获取内容。我们希望确保只有一个此类实例存在,以跟踪所有获取的 URL。

假设你在程序的多个部分有多个获取器,但你希望跟踪所有已获取的 URL。这是一个单例模式的典型用例。通过确保程序的所有部分都使用相同的获取器实例,你可以轻松地在同一位置跟踪所有已获取的 URL。

初始时,我们创建了一个简单的 URLFetcher 类。这个类有一个 fetch() 方法,用于获取网页内容并将 URL 存储在列表中:

import urllib.request
class URLFetcher:
    def __init__(self):
        self.urls = []
    def fetch(self, url):
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req) as response:
            if response.code == 200:
                page_content = response.read()
             with open("content.html", "a") as f:
                 f.write(page_content + "\n")
             self.urls.append(url)

为了检查我们的类是否是 is 操作符。如果它们相同,那么它就是一个单例:

if __name__ == "__main__":
    print(URLFetcher() is URLFetcher())

如果你运行此代码 (ch03/singleton/before_singleton.py),你会看到以下输出:

False

这个输出显示,在这个版本中,类还没有遵循单例模式。为了使其成为单例,我们将使用 元类 技术。

其他信息

Python 中的元类是一个定义了类如何行为的类。

我们将创建一个 SingletonType 元类,确保只有一个 URLFetcher 实例存在,如下所示:

import urllib.request
class SingletonType(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            obj = super(SingletonType, cls).__call__(*args, **kwargs)
            cls._instances[cls] = obj
        return cls._instances[cls]

现在,我们修改我们的 URLFetcher 类以使用这个元类,如下所示:

class URLFetcher(metaclass=SingletonType):
    def __init__(self):
        self.urls = []
    def fetch(self, url):
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req) as response:
            if response.code == 200:
                page_content = response.read()
                with open("content.html", "a") as f:
                    f.write(str(page_content))
                self.urls.append(url)

最后,我们创建了一个 main() 函数并调用它来测试我们的单例,代码如下:

def main():
    my_urls = [
            "http://python.org",
            "https://planetpython.org/",
            "https://www.djangoproject.com/",
    ]
    print(URLFetcher() is URLFetcher())
    fetcher = URLFetcher()
    for url in my_urls:
        fetcher.fetch(url)
    print(f"Done URLs: {fetcher.urls}")
if __name__ == "__main__":
    main()

下面是我们在代码中执行的操作的总结 (ch03``/singleton/singleton.py):

  1. 我们从所需的模块导入开始 (urllib.request)。

  2. 我们定义了一个 SingletonType 类,它有一个特殊的 __call__() 方法。

  3. 我们定义了 URLFetcher 类,该类实现了网页的获取器,并通过 urls 属性初始化它;如前所述,我们添加了它的 fetch() 方法。

  4. 最后,我们添加了我们的 main() 函数,并添加了 Python 中用于调用它的传统代码片段。

为了测试实现,运行 python ch03/singleton/singleton.py 命令。你应该得到以下输出:

True
Done URLs: ['http://python.org', 'https://planetpython.org/', 'https://www.djangoproject.com/']

此外,你将发现已创建了一个名为 content.html 的文件,其中包含了来自不同 URL 的 HTML 文本。

因此,程序按预期完成了任务。这是一个演示如何使用单例模式的例子。

你应该使用单例模式吗?

虽然单例模式有其优点,但它可能并不总是管理全局状态或资源的最 Pythonic 方法。我们的实现示例是有效的,但如果我们停下来再次分析代码,我们会注意到以下情况:

  • 实现所用的技术相当高级,不易向初学者解释。

  • 通过阅读 SingletonType 类的定义,如果不看名字,你可能不会立即看出它提供了一个单例的元类。

在 Python 中,开发者通常更喜欢单例的简单替代方案:使用模块级全局对象。

注意

Python 模块充当自然命名空间,可以包含变量、函数和类,这使得它们非常适合组织和共享全局资源。

通过采用全局对象技术,正如布兰登·罗德斯在其所谓的全局对象模式([python-patterns.guide/python/module-globals/](https://python-patterns.guide/python/module-globals/))中解释的那样,你可以在不需要复杂实例化过程或迫使一个类只有一个实例的情况下,达到单例模式相同的结果。

作为练习,你可以使用全局对象重写我们示例的实现。为了参考,定义全局对象的等效代码在ch03/singleton/instead_of_singleton/example.py文件中;有关其使用,请查看ch03/singleton/instead_of_singleton/use_example.py文件。

对象池模式

对象池模式是一种创建型设计模式,它允许你在需要时重用现有对象,而不是创建新的对象。这种模式在初始化新对象在系统资源、时间等方面的成本较高时特别有用。

现实世界中的例子

考虑一个汽车租赁服务。当客户租车时,服务不会为他们制造一辆新车。相反,它从可用的汽车池中提供一辆。一旦客户归还了汽车,它就会回到池中,准备好供下一个客户使用。

另一个例子是一个公共游泳池。而不是每次有人想要游泳时都往游泳池里加水,而是对水进行处理并重复使用,供多个游泳者使用。这既节省了时间又节省了资源。

对象池模式的用例

对象池模式在资源初始化成本高昂或耗时的情况下特别有用。这可能涉及 CPU 周期、内存使用,甚至网络带宽。例如,在一个射击视频游戏中,你可能会使用这种模式来管理子弹对象。每次开枪时创建一个新的子弹可能会消耗大量资源。相反,你可以有一个子弹对象池,这些对象可以重复使用。

实现对象池模式

让我们实现一个可重用car对象池,用于汽车租赁应用程序,以避免重复创建和销毁它们。

首先,我们需要定义一个Car类,如下所示:

class Car:
    def __init__(self, make: str, model: str):
        self.make = make
        self.model = model
        self.in_use = False

然后,我们开始定义一个CarPool类及其初始化,如下所示:

class CarPool:
    def __init__(self):
        self._available = []
        self._in_use = []

我们需要表达当客户端获取一辆车时会发生什么。为此,我们在类上定义了一个方法,执行以下操作:如果没有可用的汽车,我们实例化一辆并添加到池中可用的汽车列表中;否则,我们返回一个可用的car对象,同时执行以下操作:

  • car对象中的_in_use属性设置为True

  • car对象添加到“正在使用”的车辆列表中(存储在pool对象的_in_use属性中)

我们将那个方法的代码添加到类中,如下所示:

    def acquire_car(self) -> Car:
        if len(self._available) == 0:
            new_car = Car("BMW", "M3")
            self._available.append(new_car)
        car = self._available.pop()
        self._in_use.append(car)
        car.in_use = True
        return car

然后我们添加了一个处理客户释放车辆时的方法,如下所示:

    def release_car(self, car: Car) -> None:
        car.in_use = False
        self._in_use.remove(car)
        self._available.append(car)

最后,我们添加了一些测试实现结果的代码,如下所示:

if __name__ == "__main__":
    pool = CarPool()
    car_name = "Car 1"
    print(f"Acquire {car_name}")
    car1 = pool.acquire_car()
    print(f"{car_name} in use: {car1.in_use}")
    print(f"Now release {car_name}")
    pool.release_car(car1)
    print(f"{car_name} in use: {car1.in_use}")

下面是我们在代码中执行的操作的摘要(在文件ch03/object_pool.py中):

  1. 我们定义了一个Car类。

  2. 我们定义了一个带有acquire_car()release_car()方法的CarPool类,如前所述。

  3. 我们添加了测试实现结果的代码,如前所述。

要测试程序,请运行以下命令:

python ch03/object_pool.py

你应该得到以下输出:

Acquire Car 1
Car 1 in use: True
Now release Car 1
Car 1 in use: False

干得好!这个输出表明我们的对象池模式实现按预期工作。

摘要

在本章中,我们看到了创建型设计模式,这对于构建灵活、可维护和模块化的代码至关重要。我们通过检查工厂模式的两种变体开始了本章,每种变体都为对象创建提供了独特的优势。接下来,我们探讨了构建者模式,它提供了一种更易读、更易于维护的方式来构建复杂对象。随后,原型模式引入了一种高效克隆对象的方法。最后,我们通过讨论单例和对象池模式结束了本章,这两种模式都旨在优化资源管理并确保应用程序中状态的一致性。

现在,我们拥有了这些对象创建的基础模式,我们为下一章做好了准备,我们将发现结构性设计模式。

第四章:结构设计模式

在上一章中,我们介绍了创建型模式和面向对象编程模式,这些模式帮助我们处理对象创建过程。接下来,我们想要介绍的模式类别是结构设计模式。结构设计模式提出了一种组合对象以提供新功能的方法。

本章我们将涵盖以下主要内容:

  • 适配器模式

  • 装饰器模式

  • 桥接模式

  • 门面模式

  • 享元模式

  • 代理模式

在本章结束时,你将掌握使用结构设计模式高效且优雅地构建代码的技能。

技术要求

请参阅第一章中提出的需求。

适配器模式

适配器模式是一种结构设计模式,它帮助我们使两个不兼容的接口变得兼容。这究竟意味着什么?如果我们有一个旧组件,我们想在新的系统中使用它,或者我们想在旧系统中使用的新组件,这两个组件在没有进行任何代码更改的情况下很少能够相互通信。但是,更改代码并不总是可能的,要么是因为我们没有访问权限,要么是因为这样做不切实际。在这种情况下,我们可以编写一个额外的层,它会对所有必要的修改进行操作,以使两个接口之间能够通信。这个层被称为适配器

通常,如果你想要使用一个期望function_a()的接口,但你只有function_b(),你可以使用适配器将(适配)function_b()转换为function_a()

现实世界案例

当你从大多数欧洲国家前往英国或美国,或者相反,你需要使用一个插头适配器来给你的笔记本电脑充电。连接某些设备到你的电脑也需要同种类型的适配器:USB 适配器。

在软件类别中,zope.interface包(pypi.org/project/zope.interface/),是Zope 工具包ZTK)的一部分,提供了帮助定义接口和执行接口适配的工具。这些工具被用于几个 Python 网络框架项目的核心(包括 Pyramid 和 Plone)。

注意

zope.interface是 Python 中处理接口的解决方案,由 Zope 应用程序服务器和 ZTK 背后的团队提出,在 Python 引入内置机制之前,首先提出了抽象基类(也称为ABCs),后来又提出了协议。

适配器模式的使用案例

通常,两个不兼容的接口中有一个是外来的,或者是旧的/遗留的。如果接口是外来的,这意味着我们没有访问源代码。如果是旧的,通常重构它是不切实际的。

在实现之后使用适配器使事物工作是一种好方法,因为它不需要访问外部接口的源代码。如果我们必须重用一些遗留代码,这通常也是一个实用的解决方案。但要注意,它可能会引入难以调试的副作用。因此,请谨慎使用。

实现适配器模式——适配遗留类

让我们考虑一个例子,其中我们有一个遗留的支付系统和一个新的支付网关。适配器模式可以使它们在不更改现有代码的情况下一起工作,正如我们将要看到的。

遗留支付系统使用一个类实现,包含一个make_payment()方法,用于执行支付的核心工作,如下所示:

class OldPaymentSystem:
    def __init__(self, currency):
        self.currency = currency
    def make_payment(self, amount):
        print(
            f"[OLD] Pay {amount} {self.currency}"
        )

新的支付系统使用以下类实现,提供了一个execute_payment()方法:

class NewPaymentGateway:
    def __init__(self, currency):
        self.currency = currency
    def execute_payment(self, amount):
        print(
            f"Execute payment of {amount} {self.currency}"
        )

现在,我们将添加一个类,它将提供make_payment()方法,在这个方法中,我们在适配对象上调用execute_payment()方法来完成支付。代码如下:

class PaymentAdapter:
    def __init__(self, system):
        self.system = system
    def make_payment(self, amount):
        self.system.execute_payment(amount)

这就是PaymentAdapter类如何适配NewPaymentGateway的接口以匹配OldPaymentSystem的接口。

让我们通过添加一个main()函数并包含测试代码来查看这种适配的结果,如下所示:

def main():
    old_system = OldPaymentSystem("euro")
    print(old_system)
    new_system = NewPaymentGateway("euro")
    print(new_system)
    adapter = PaymentAdapter(new_system)
    adapter.make_payment(100)

让我们回顾一下实现的全代码(见ch04/adapter/adapt_legacy.py文件):

  1. 我们有一些遗留支付系统的代码,由OldPaymentSystem类表示,它提供了一个make_payment()方法,用于执行支付的核心工作,如下所示:

  2. 我们引入了新的支付系统,使用NewPaymentGateway类,它提供了一个execute_payment()方法。

  3. 我们添加了一个适配器类PaymentAdapter,它有一个属性用于存储支付系统对象,以及一个make_payment()方法;在该方法中,我们在支付系统对象上调用execute_payment()方法(通过self.system.execute_payment(amount))。

  4. 我们添加了测试我们接口适配设计的代码(并在常规的if __name__ == "__main__"块中调用它)。

执行代码,使用python ch04/adapter/adapt_legacy.py,应该得到以下输出:

<__main__.OldPaymentSystem object at 0x10ee58fd0>
<__main__.NewPaymentGateway object at 0x10ee58f70>
Execute payment of 100 euro

你现在明白了。这种适配技术使我们能够使用新的支付网关,同时使用期望旧接口的现有代码。

实现适配器模式——将几个类适配到统一接口

让我们看看另一个应用来展示适配的例子:一个俱乐部的活动。我们俱乐部有两个主要活动:

  • 聘请有才华的艺术家在俱乐部表演

  • 组织表演和活动以娱乐其客户

在核心,我们有一个Club类,它代表俱乐部,聘请的艺术家在某个晚上表演。organize_performance()方法是俱乐部可以执行的主要动作。代码如下:

class Club:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return f"the club {self.name}"
    def organize_event(self):
        return "hires an artist to perform"

大多数时候,我们的俱乐部聘请 DJ 表演,但我们的应用程序应该能够组织多样化的表演:由音乐家或音乐乐队、舞者、单人或双人表演等。

通过我们的研究尝试重用现有代码,我们发现了一个开源贡献的库,它为我们带来了两个有趣的类:MusicianDancer。在Musician类中,主要动作由play()方法执行。在Dancer类中,由dance()方法执行。

在我们的例子中,为了表明这两个类是外部的,我们将它们放在一个单独的模块中(在ch04/adapter/external.py文件中)。代码包括两个类,MusicianDancer,如下所示:

class Musician:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return f"the musician {self.name}"
    def play(self):
        return "plays music"
class Dancer:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return f"the dancer {self.name}"
    def dance(self):
        return "does a dance performance"

我们编写的代码,用于从外部库使用这两个类,只知道如何调用organize_performance()方法(在Club类上);它对play()dance()方法(在相应类上)一无所知。

我们如何在不修改MusicianDancer类的情况下让代码工作?

适配器来拯救!我们创建了一个通用的Adapter类,它允许我们将具有不同接口的多个对象适配到一个统一的接口。__init__()方法的obj参数是我们想要适配的对象,adapted_methods是一个包含键/值对的字典,匹配客户端调用的方法和应该调用的方法。该类的代码如下:

class Adapter:
    def __init__(self, obj, adapted_methods):
        self.obj = obj
        self.__dict__.update(adapted_methods)
    def __str__(self):
        return str(self.obj)

当处理不同类的实例时,我们有两种情况:

  • 属于Club类的兼容对象不需要适配。我们可以将其视为原样。

  • 不兼容的对象需要首先使用Adapter类进行适配。

结果是,客户端代码可以在所有对象上继续使用已知的organize_performance()方法,而无需意识到任何接口差异。考虑以下main()函数代码以证明设计按预期工作:

def main():
    objects = [
        Club("Jazz Cafe"),
        Musician("Roy Ayers"),
        Dancer("Shane Sparks"),
    ]
    for obj in objects:
        if hasattr(obj, "play") or hasattr(
            obj, "dance"
        ):
            if hasattr(obj, "play"):
                adapted_methods = dict(
                    organize_event=obj.play
                )
            elif hasattr(obj, "dance"):
                adapted_methods = dict(
                    organize_event=obj.dance
                )
            obj = Adapter(obj, adapted_methods)
        print(f"{obj} {obj.organize_event()}")

让我们回顾一下我们适配器模式实现的完整代码(在ch04/adapter/adapt_to_unified_interface.py文件中):

  1. 我们从external模块中导入MusicianDancer类。

  2. 我们有Club类。

  3. 我们定义了Adapter类。

  4. 我们添加了main()函数,我们在通常的if __name__ == "__main__"块中调用它。

执行python ch04/adapter/adapt_to_unified_interface.py命令时的输出如下:

the club Jazz Cafe hires an artist to perform
the musician Roy Ayers plays music
the dancer Shane Sparks does a dance performance

如你所见,我们成功地使MusicianDancer类与客户端代码期望的接口兼容,而没有改变这些外部类的源代码。

装饰器模式

另一个值得学习的有趣结构模式是装饰器模式,它允许程序员以动态和透明的方式(不影响其他对象)向对象添加职责。

这个模式对我们来说还有一个有趣的原因,你将在下一分钟看到。

作为 Python 开发者,我们可以以Pythonic的方式(意味着使用语言的功能)编写装饰器,多亏了内置的装饰器功能。

注意

Python 装饰器是一个可调用对象(函数、方法或类),它接收一个func_in函数对象作为输入,并返回另一个函数对象func_out。这是一种常用的技术,用于扩展函数、方法或类的行为。

有关 Python 装饰器功能的更多详细信息,请参阅官方文档:docs.python.org/3/reference/compound_stmts.html#function

但这个特性不应该对你来说完全陌生。我们在前面的章节中已经遇到了常用的装饰器(@abstractmethod@property),Python 中还有几个其他有用的内置装饰器。现在,我们将学习如何实现和使用我们自己的装饰器。

注意,装饰器模式和 Python 的装饰器功能之间没有一对一的关系。Python 装饰器实际上可以做很多装饰器模式做不到的事情。它们可以用作实现装饰器模式的事情之一。

现实世界的例子

装饰器模式通常用于扩展对象的功能。在日常生活中,此类扩展的例子包括给枪支添加消声器、使用不同的相机镜头等。

在像 Django 这样的 Web 框架中,它大量使用装饰器,我们有以下用途的装饰器:

  • 基于请求限制对视图(或 HTTP 请求处理函数)的访问

  • 在特定视图中控制缓存行为

  • 基于视图的压缩控制

  • 基于特定 HTTP 请求头控制缓存

  • 将函数注册为事件订阅者

  • 使用特定权限保护函数

装饰器模式的用例

当用于实现横切关注点时,装饰器模式特别有用,例如以下内容:

  • 数据验证

  • 缓存

  • 记录日志

  • 监控

  • 调试

  • 业务规则

  • 加密

通常,一个应用程序中所有通用且可以应用于其许多其他部分的组成部分都被认为是横切关注点。

使用装饰器模式的一个流行例子是在图形用户界面GUI)工具包中。在 GUI 工具包中,我们希望能够向单个组件/小部件添加诸如边框、阴影、颜色和滚动等功能。

实现装饰器模式

Python 装饰器是通用的且非常强大。在本节中,我们将看到如何实现一个number_sum()函数,该函数返回前n个数字的总和。请注意,此函数已经在math模块中作为fsum()提供,但让我们假装它不存在。

首先,让我们看看这个简单的实现(在ch04/decorator/number_sum_naive.py文件中):

def number_sum(n):
    if n == 0:
        return 0
    else:
        return n + number_sum(n - 1)
if __name__ == "__main__":
    from timeit import Timer
    t = Timer(
        "number_sum(50)",
        "from __main__ import number_sum",
    )
    print("Time: ", t.timeit())

此示例的执行样本显示了这种实现的缓慢程度。在我的电脑上,计算前 50 个数字之和需要超过7秒。执行python ch04/decorator/number_sum_naive.py命令时,我们得到以下输出:

dict for caching the already computed sums. We also change the parameter passed to the number_sum() function. We want to calculate the sum of the first 300 numbers instead of only the first 50.
			Here is the new version of the code (in the `ch04``/decorator/number_sum.py` file), using memoization:

sum_cache = {0: 0}

def number_sum(n):

if n in sum_cache:

return sum_cache[n]

res = n + number_sum(n - 1)

Add the value to the cache

sum_cache[n] = res

return res

if name == "main":

from timeit import Timer

t = Timer(

"number_sum(300)",

"from main import number_sum",

)

print("Time: ", t.timeit())


			Executing the memoization-based code shows that performance improves dramatically, and is acceptable even for computing large values.
			A sample execution, using `python ch04/decorator/number_sum.py`, is as follows:

Time:  0.1288748119986849


			But there are a few problems with this approach. First, while the performance is not an issue any longer, the code is not as clean as it is when not using memoization. And what happens if we decide to extend the code with more math functions and turn it into a module? We can think of several functions that would be useful for our module, for problems such as Pascal’s triangle or the Fibonacci numbers suite algorithm.
			So, if we wanted a function in the same module as `number_sum()` for the Fibonacci numbers suite, using the same memoization technique, we would add code as follows (see the version in the `ch04/decorator/number_sum_and_fibonacci.py` file):

fib_cache = {0: 0, 1: 1}

def fibonacci(n):

if n in fib_cache:

return fib_cache[n]

res = fibonacci(n - 1) + fibonacci(n - 2)

fib_cache[n] = res

return res


			Do you notice the problem? We ended up with a new dictionary called `fib_cache` that acts as our cache for the `fibonacci()` function, and a function that is more complex than it would be without using memoization. Our module is becoming unnecessarily complex.
			Is it possible to write these functions while keeping them as simple as the naive versions, but achieving a performance similar to the performance of the functions that use memoization?
			Fortunately, it is, and the solution is to use the decorator pattern.
			First, we create a `memoize()` decorator as shown in the following example. Our decorator accepts the `func` function, which needs to be memoized, as an input. It uses `dict` named `cache` as the cached data container. The `functools.wraps()` function is used for convenience when creating decorators. It is not mandatory but it’s a good practice to use it, since it makes sure that the documentation and the signature of the function that is decorated are preserved. The `*args` argument list is required in this case because the functions that we want to decorate accept input arguments (such as the `n` argument for our two functions):

import functools

def memoize(func):

cache = {}

@functools.wraps(func)

def memoizer(*args):

if args not in cache:

cache[args] = func(*args)

return cache[args]

return memoizer


			Now we can use our `memoize()` decorator with the naive version of our functions. This has the benefit of readable code without performance impact. We apply a decorator using what is known as `@name` syntax, where `name` is the name of the decorator that we want to use. It is nothing more than syntactic sugar for simplifying the usage of decorators. We can even bypass this syntax and execute our decorator manually, but that is left as an exercise for you.
			So, the `memoize()` decorator can be used with our recursive functions as follows:

@memoize

def number_sum(n):

if n == 0:

return 0

else:

return n + number_sum(n - 1)

@memoize

def fibonacci(n):

if n in (0, 1):

return n

else:

return fibonacci(n - 1) + fibonacci(n - 2)


			In the last part of the code, via the `main()` function, we show how to use the decorated functions and measure their performance. The `to_execute` variable is used to hold a list of tuples containing the reference to each function and the corresponding `timeit.Timer()` call (to execute it while measuring the time spent), thus avoiding code repetition. Note how the `__name__` and `__doc__` method attributes show the proper function names and documentation values, respectively. Try removing the `@functools.wraps(func)` decoration from `memoize()` and see whether this is still the case.
			Here is the last part of the code:

def main():

from timeit import Timer

to_execute = [

(

number_sum,

Timer(

"number_sum(300)",

"from main import number_sum",

),

),

(

fibonacci,

Timer(

"fibonacci(100)",

"from main import fibonacci",

),

),

]

for item in to_execute:

func = item[0]

print(

f'Function "{func.name}": {func.doc}'

)

t = item[1]

print(f"Time: {t.timeit()}")

print()


			Let’s recapitulate how we write the complete code of our math module (the `ch04/decorator/decorate_math.py` file):

				1.  After the import of Python’s `functools` module that we will be using, we define the `memoize()` decorator function.
				2.  Then, we define the `number_sum()` function, decorated using `memoize()`.
				3.  Next, we define the `fibonacci()` function, decorated the same way.
				4.  Finally, we add the `main()` function, as shown earlier, and the usual trick to call it.

			Here is a sample output when executing the `python` `ch04/decorator/decorate_math.py` command:

Function "number_sum": Returns the sum of the first n numbers

Time: 0.2148694

Function "fibonacci": Returns the suite of Fibonacci numbers

Time: 0.202763251


			Note
			The execution times might differ in your case. Also, regardless of the time spent, we can see that the decorator-based implementation is a win because the code is more maintainable.
			Nice! We ended up with readable code and acceptable performance. Now, you might argue that this is not the decorator pattern, since we don’t apply it at runtime. The truth is that a decorated function cannot be undecorated, but you can still decide at runtime whether the decorator will be executed or not. That’s an interesting exercise left for you. *Hint for the exercise:* use a decorator that acts as a wrapper, which decides whether or not the real decorator is executed based on some condition.
			The bridge pattern
			A third structural pattern to look at is the **bridge** pattern. We can actually compare the bridge and the adapter patterns, looking at the way both work. While the adapter pattern is used *later* to make unrelated classes work together, as we saw in the implementation example we discussed earlier in the section on *The adapter pattern*, the bridge pattern is designed *up-front* to decouple an implementation from its abstraction, as we are going to see.
			Real-world examples
			In our modern, everyday lives, an example of the bridge pattern I can think of is from the *digital economy*: information products. Nowadays, the information product or *infoproduct* is part of the resources one can find online for training, self-improvement, or one’s ideas and business development. The purpose of an information product that you find on certain marketplaces, or the website of the provider, is to deliver information on a given topic in such a way that it is easy to access and consume. The provided material can be a PDF document or ebook, an ebook series, a video, a video series, an online course, a subscription-based newsletter, or a combination of all those formats.
			In the software realm, we can find two examples:

				*   **Device drivers**: Developers of an OS define the interface for device (such as printers) vendors to implement it
				*   **Payment gateways**: Different payment gateways can have different implementations, but the checkout process remains consistent

			Use cases for the bridge pattern
			Using the bridge pattern is a good idea when you want to share an implementation among multiple objects. Basically, instead of implementing several specialized classes, and defining all that is required within each class, you can define the following special components:

				*   An abstraction that applies to all the classes
				*   A separate interface for the different objects involved

			An implementation example we are about to see will illustrate this approach.
			Implementing the bridge pattern
			Let’s assume we are building an application where the user is going to manage and deliver content after fetching it from diverse sources, which could be the following:

				*   A web page (based on its URL)
				*   A resource accessed on an FTP server
				*   A file on the local filesystem
				*   A database server

			So, here is the idea: instead of implementing several content classes, each holding the methods responsible for getting the content pieces, assembling them, and showing them inside the application, we can define an abstraction for the *Resource Content* and a separate interface for the objects that are responsible for fetching the content. Let’s try it!
			We begin with the interface for the implementation classes that help fetch content – that is, the `ResourceContentFetcher` class. This concept is called the `protocols` feature, as follows:

class ResourceContentFetcher(Protocol):

def fetch(self, path: str) -> str:

...


			Then, we define the class for our Resource Content abstraction, called `ResourceContent`. The first trick we use here is that, via an attribute (`_imp`) on the `ResourceContent` class, we maintain a reference to the object that represents the Implementor (fulfilling the `ResourceContentFetcher` interface). The code is as follows:

class ResourceContent:

def init(self, imp: ResourceContentFetcher):

self._imp = imp

def get_content(self, path):

return self._imp.fetch(path)


			Now we can add an `implementation` class to fetch content from a web page or resource:

class URLFetcher:

def fetch(self, path):

res = ""

req = urllib.request.Request(path)

with urllib.request.urlopen(

req

) as response:

if response.code == 200:

res = response.read()

return res


			We can also add an `implementation` class to fetch content from a file on the local filesystem:

class LocalFileFetcher:

def fetch(self, path):

with open(path) as f:

res = f.read()

return res


			Based on that, a `main` function with some testing code to show content using both *content fetchers* could look like the following:

def main():

url_fetcher = URLFetcher()

rc = ResourceContent(url_fetcher)

res = rc.get_content("http://python.org")

print(

f"Fetched content with {len(res)} characters"

)

localfs_fetcher = LocalFileFetcher()

rc = ResourceContent(localfs_fetcher)

pathname = os.path.abspath(file)

dir_path = os.path.split(pathname)[0]

path = os.path.join(dir_path, "file.txt")

res = rc.get_content(path)

print(

f"Fetched content with {len(res)} characters"

)


			Let’s see a summary of the complete code of our example (the `ch04/bridge/bridge.py` file):

				1.  We import the modules we need for the program (`os`, `urllib.request`, and `typing.Protocol`).
				2.  We define the `ResourceContentFetcher` interface, using *protocols*, for the *Implementor*.
				3.  We define the `ResourceContent` class for the interface of the abstraction.
				4.  We define two implementation classes:
    *   `URLFetcher` for fetching content from a URL
    *   `LocalFileFetcher` for fetching content from the local filesystem
				5.  Finally, we add the `main()` function, as shown earlier, and the usual trick to call it.

			Here is a sample output when executing the `python` `ch04/bridge/bridge.py` command:

Fetched content with 51265 characters

Fetched content with 1327 characters


			This is a basic illustration of how using the bridge pattern in your design, you can extract content from different sources and integrate the results in the same data manipulation system or user interface.
			The facade pattern
			As systems evolve, they can get very complex. It is not unusual to end up with a very large (and sometimes confusing) collection of classes and interactions. In many cases, we don’t want to expose this complexity to the client. This is where our next structural pattern comes to the rescue: **facade**.
			The facade design pattern helps us hide the internal complexity of our systems and expose only what is necessary to the client through a simplified interface. In essence, facade is an abstraction layer implemented over an existing complex system.
			Let’s take the example of the computer to illustrate things. A computer is a complex machine that depends on several parts to be fully functional. To keep things simple, the word “computer,” in this case, refers to an IBM derivative that uses a von Neumann architecture. Booting a computer is a particularly complex procedure. The CPU, main memory, and hard disk need to be up and running, the boot loader must be loaded from the hard disk to the main memory, the CPU must boot the operating system kernel, and so forth. Instead of exposing all this complexity to the client, we create a facade that encapsulates the whole procedure, making sure that all steps are executed in the right order.
			In terms of object design and programming, we should have several classes, but only the `Computer` class needs to be exposed to the client code. The client will only have to execute the `start()` method of the `Computer` class, for example, and all the other complex parts are taken care of by the facade `Computer` class.
			Real-world examples
			The facade pattern is quite common in life. When you call a bank or a company, you are usually first connected to the customer service department. The customer service employee acts as a facade between you and the actual department (billing, technical support, general assistance, and so on), where an employee will help you with your specific problem.
			As another example, a key used to turn on a car or motorcycle can also be considered a facade. It is a simple way of activating a system that is very complex internally. And, of course, the same is true for other complex electronic devices that we can activate with a single button, such as computers.
			In software, the `django-oscar-datacash` module is a Django third-party module that integrates with the **DataCash** payment gateway. The module has a gateway class that provides fine-grained access to the various DataCash APIs. On top of that, it also offers a facade class that provides a less granular API (for those who don’t want to mess with the details), and the ability to save transactions for auditing purposes.
			The `Requests` library is another great example of the facade pattern. It simplifies sending HTTP requests and handling responses, abstracting the complexities of the HTTP protocol. Developers can easily make HTTP requests without dealing with the intricacies of sockets or the underlying HTTP methods.
			Use cases for the facade pattern
			The most usual reason to use the facade pattern is to provide a single, simple entry point to a complex system. By introducing facade, the client code can use a system by simply calling a single method/function. At the same time, the internal system does not lose any functionality, it just encapsulates it.
			Not exposing the internal functionality of a system to the client code gives us an extra benefit: we can introduce changes to the system, but the client code remains unaware of and unaffected by the changes. No modifications are required to the client code.
			Facade is also useful if you have more than one layer in your system. You can introduce one facade entry point per layer and let all layers communicate with each other through their facades. That promotes **loose coupling** and keeps the layers as independent as possible.
			Implementing the facade pattern
			Assume that we want to create an operating system using a multi-server approach, similar to how it is done in MINIX 3 or GNU Hurd. A multi-server operating system has a minimal kernel, called the **microkernel**, which runs in privileged mode. All the other services of the system are following a server architecture (driver server, process server, file server, and so forth). Each server belongs to a different memory address space and runs on top of the microkernel in user mode. The pros of this approach are that the operating system can become more fault-tolerant, reliable, and secure. For example, since all drivers are running in user mode on a driver server, a bug in a driver cannot crash the whole system, nor can it affect the other servers. The cons of this approach are the performance overhead and the complexity of system programming, because the communication between a server and the microkernel, as well as between the independent servers, happens using message passing. Message passing is more complex than the shared memory model used in monolithic kernels such as Linux.
			We begin with a `Server` interface. Also, an `Enum` parameter describes the different possible states of a server. We use the `ABC` technique to forbid direct instantiation of the `Server` interface and make the fundamental `boot()` and `kill()` methods mandatory, assuming that different actions are needed to be taken for booting, killing, and restarting each server. Here is the code for these elements, the first important bits to support our implementation:

State = Enum(

"State",

"NEW RUNNING SLEEPING RESTART ZOMBIE",

)

...

class Server(ABC):

@abstractmethod

def init(self):

pass

def str(self):

return self.name

@abstractmethod

def boot(self):

pass

@abstractmethod

def kill(self, restart=True):

pass


			A modular operating system can have a great number of interesting servers: a file server, a process server, an authentication server, a network server, a graphical/window server, and so forth. The following example includes two stub servers: `FileServer` and `ProcessServer`. Apart from the `boot()` and `kill()` methods all servers have, `FileServer` has a `create_file()` method for creating files, and `ProcessServer` has a `create_process()` method for creating processes.
			The `FileServer` class is as follows:

class FileServer(Server):

def init(self):

self.name = "FileServer"

self.state = State.NEW

def boot(self):

print(f"booting the {self}")

self.state = State.RUNNING

def kill(self, restart=True):

print(f"Killing {self}")

self.state = (

State.RESTART if restart else State.ZOMBIE

)

def create_file(self, user, name, perms):

msg = (

f"尝试创建文件 '{name}' "

f"for user '{user}' "

f"权限为 {perms}"

)

print(msg)


			The `ProcessServer` class is as follows:

class ProcessServer(Server):

def init(self):

self.name = "ProcessServer"

self.state = State.NEW

def boot(self):

print(f"启动 {self}")

self.state = State.RUNNING

def kill(self, restart=True):

print(f"杀死 {self}")

self.state = (

State.RESTART if restart else State.ZOMBIE

)

def create_process(self, user, name):

msg = (

f"尝试创建进程 '{name}' "

f"for user '{user}'"

)

print(msg)


			The `OperatingSystem` class is a facade. In its `__init__()`, all the necessary server instances are created. The `start()` method, used by the client code, is the entry point to the system. More wrapper methods can be added, if necessary, as access points to the services of the servers, such as the wrappers, `create_file()` and `create_process()`. From the client’s point of view, all those services are provided by the `OperatingSystem` class. The client should not be confused by unnecessary details such as the existence of servers and the responsibility of each server.
			The code for the `OperatingSystem` class is as follows:

class OperatingSystem:

"""门面模式”

def init(self):

self.fs = FileServer()

self.ps = ProcessServer()

def start(self):

[i.boot() for i in (self.fs, self.ps)]

def create_file(self, user, name, perms):

return self.fs.create_file(user, name, perms)

def create_process(self, user, name):

return self.ps.create_process(user, name)


			As you are going to see in a minute, when we present a summary of the example, there are many dummy classes and servers. They are there to give you an idea about the required abstractions (`User`, `Process`, `File`, and so forth) and servers (`WindowServer`, `NetworkServer`, and so forth) for making the system functional.
			Finally, we add our main code for testing the design, as follows:

def main():

os = OperatingSystem()

os.start()

os.create_file("foo", "hello.txt", "-rw-r-r")

os.create_process("bar", "ls /tmp")


			We are going to recapitulate the details of our implementation example; the full code is in the `ch04/facade.py` file:

				1.  We start with the imports we need.
				2.  We define the `State` constant using `Enum`, as shown earlier.
				3.  We then add the `User`, `Process`, and `File` classes, which do nothing in this minimal but functional example.
				4.  We define the abstract `Server` class, as shown earlier.
				5.  We then define the `FileServer` class and the `ProcessServer` class, which are both subclasses of `Server`.
				6.  We add two other dummy classes, `WindowServer` and `NetworkServer`.
				7.  Then we define our facade class, `OperatingSystem`, as shown earlier.
				8.  Finally, we add the main part of the code, where we use the facade we have defined.

			As you can see, executing the `python ch04/facade.py` command shows the messages produced by our two stub servers:

启动文件服务器

启动 ProcessServer

尝试为用户 'foo' 创建文件 'hello.txt',权限为 -rw-r-r

操作系统类做得很好。客户端代码可以创建文件和进程,而无需了解操作系统内部细节,例如多个服务器的存在。更准确地说,客户端代码可以调用创建文件和进程的方法,但它们目前是虚拟的。作为一个有趣的练习,你可以实现这两种方法中的一种,甚至两种都可以。

        轻量级模式

        每当我们创建一个新的对象时,都需要额外分配内存。虽然虚拟内存从理论上为我们提供了无限的内存,但现实情况并非如此。如果一个系统的所有物理内存都用完了,它将开始与辅助存储(通常是**硬盘驱动器**(**HDD**))交换页面,由于主内存和 HDD 之间的性能差异,这在大多数情况下是不可接受的。**固态驱动器**(**SSD**)通常比 HDD 有更好的性能,但并不是每个人都期望使用 SSD。因此,SSD 不太可能在不久的将来完全取代 HDD。

        除了内存使用外,性能也是一个考虑因素。图形软件,包括计算机游戏,应该能够非常快速地渲染 3-D 信息(例如,有成千上万树木的森林,满是士兵的村庄,或者有很多汽车的城区)。如果 3-D 地形中的每个对象都是单独创建的,并且没有使用数据共享,性能将会非常低。

        作为软件工程师,我们应该通过编写更好的软件来解决软件问题,而不是强迫客户购买额外的或更好的硬件。**轻量级**设计模式是一种技术,通过在相似对象之间引入数据共享来最小化内存使用并提高性能。轻量级对象是一个包含状态无关、不可变(也称为**内在**)数据的共享对象。状态相关、可变(也称为**外在**)数据不应成为轻量级对象的一部分,因为这是无法共享的信息,因为它在每个对象中都是不同的。如果轻量级对象需要外在数据,它应该由客户端代码显式提供。

        以下是一个例子,可以帮助阐明如何实际使用轻量级模式。假设我们正在创建一个性能关键的游戏——例如,一个**第一人称射击游戏**(**FPS**)。在 FPS 游戏中,玩家(士兵)共享一些状态,例如表示和行为。例如,在《反恐精英》中,同一队的所有士兵(反恐分子与恐怖分子)看起来都一样(表示)。在同一个游戏中,所有士兵(两队)都有一些共同的动作,如跳跃、蹲下等(行为)。这意味着我们可以创建一个包含所有共同数据的轻量级对象。当然,士兵们也有许多不同的数据,这些数据对于每个士兵来说都是独特的,并且不会成为轻量级对象的一部分,例如武器、健康、位置等。

        现实世界中的例子

        轻量级模式是一种优化设计模式;因此,在非计算领域中很难找到一个好的例子。我们可以将轻量级模式视为现实生活中的缓存。例如,许多书店都有专门的书架,用于存放最新和最受欢迎的出版物。这是一个缓存。首先,你可以查看你正在寻找的书的专门书架,如果你找不到,你可以请书店老板帮忙。

        Exaile 音乐播放器使用轻量级模式来重用对象(在这种情况下,是音乐曲目),这些对象通过相同的 URL 进行标识。如果对象与现有对象具有相同的 URL,就没有必要创建新的对象,因此可以重用相同的对象以节省资源。

        轻量级模式的使用场景

        轻量级模式(Flyweight)主要关注性能和内存使用的提升。所有嵌入式系统(如手机、平板电脑、游戏机、微控制器等)以及性能关键的应用程序(如游戏、3-D 图形处理、实时系统等)都可以从中受益。

        《设计模式:可复用面向对象软件的基础》(*Gang of Four*,*GoF*)一书中列出了以下需要满足的要求,才能有效地使用轻量级模式:

            +   应用程序需要使用大量的对象。

            +   有如此多的对象,存储/渲染它们会非常昂贵。一旦移除了可变状态(因为如果需要,应该由客户端代码显式传递给轻量级对象),许多不同的对象组可以被相对较少的共享对象所替代。

            +   对象标识对于应用来说并不重要。我们不能依赖于对象标识,因为对象共享会导致标识比较失败(对客户端代码看起来不同的对象最终会有相同的标识)。

        实现享元模式

        让我们看看我们如何实现一个包含区域的汽车示例。我们将创建一个小型停车场来展示这个想法,确保整个输出在单个终端页面上可读。然而,无论停车场有多大,内存分配保持不变。

        缓存与享元模式的比较

        缓存是一种优化技术,它使用缓存来避免重新计算在早期执行步骤中已经计算过的结果。缓存并不专注于特定的编程范式,如**面向对象编程**(**OOP**)。在 Python 中,缓存可以应用于类方法和简单函数。

        享元是一种特定于面向对象编程的优化设计模式,它专注于共享对象数据。

        让我们开始编写这个示例的代码。

        首先,我们需要一个`Enum`参数来描述停车场中存在的三种不同类型的汽车:
CarType = Enum(
    "CarType", "SUBCOMPACT COMPACT SUV"
)
        然后,我们将定义我们实现的核心类:`Car`。`pool`变量是对象池(换句话说,我们的缓存)。请注意,`pool`是一个类属性(一个所有实例共享的变量)。

        使用在`__init__()`之前被调用的特殊方法`__new__()`,我们将`Car`类转换为一个支持自引用的元类。这意味着`cls`引用了`Car`类。当客户端代码创建`Car`实例时,它们会传递汽车的类型作为`car_type`。汽车的类型用于检查是否已经创建了相同类型的汽车。如果是这样,则返回先前创建的对象;否则,将新的汽车类型添加到池中并返回:
class Car:
    pool = dict()
    def __new__(cls, car_type):
        obj = cls.pool.get(car_type, None)
        if not obj:
            obj = object.__new__(cls)
            cls.pool[car_type] = obj
            obj.car_type = car_type
        return obj
        `render()`方法将用于在屏幕上渲染汽车。注意,所有未知于享元的信息都需要客户端代码显式传递。在这种情况下,每个汽车使用随机的`color`和位置的坐标(形式为`x`,`y`)。

        此外,请注意,为了使`render()`更有用,必须确保没有汽车渲染在彼此之上。把这当作一个练习。如果你想使渲染更有趣,可以使用图形工具包,如 Tkinter、Pygame 或 Kivy。

        `render()`方法定义如下:
    def render(self, color, x, y):
        type = self.car_type
        msg = f"render a {color} {type.name} car at ({x}, {y})"
        print(msg)
        `main()`函数展示了如何使用轻量级模式。汽车的颜色是从预定义颜色列表中随机选择的值。坐标使用 1 到 100 之间的随机值。尽管渲染了 18 辆车,但只分配了 3 个内存。输出中的最后一行证明,在使用轻量级模式时,我们不能依赖于对象身份。`id()`函数返回对象的内存地址。这不是 Python 的默认行为,因为默认情况下,`id()`为每个对象返回一个唯一的 ID(实际上是对象的内存地址的整数)。在我们的情况下,即使两个对象看起来不同,如果它们属于同一个`car_type`,它们实际上具有相同的身份。当然,仍然可以使用不同的身份比较来比较不同家族的对象,但这只有在客户端知道实现细节的情况下才可能。

        我们的示例`main()`函数的代码如下:
def main():
    rnd = random.Random()
    colors = [
        "white",
        "black",
        "silver",
        "gray",
        "red",
        "blue",
        "brown",
        "beige",
        "yellow",
        "green",
    ]
    min_point, max_point = 0, 100
    car_counter = 0
    for _ in range(10):
        c1 = Car(CarType.SUBCOMPACT)
        c1.render(
            random.choice(colors),
            rnd.randint(min_point, max_point),
            rnd.randint(min_point, max_point),
        )
        car_counter += 1
    for _ in range(3):
        c2 = Car(CarType.COMPACT)
        c2.render(
            random.choice(colors),
            rnd.randint(min_point, max_point),
            rnd.randint(min_point, max_point),
        )
        car_counter += 1
    for _ in range(5):
        c3 = Car(CarType.SUV)
        c3.render(
            random.choice(colors),
            rnd.randint(min_point, max_point),
            rnd.randint(min_point, max_point),
        )
        car_counter += 1
    print(f"cars rendered: {car_counter}")
    print(
        f"cars actually created: {len(Car.pool)}"
    )
    c4 = Car(CarType.SUBCOMPACT)
    c5 = Car(CarType.SUBCOMPACT)
    c6 = Car(CarType.SUV)
    print(
        f"{id(c4)} == {id(c5)}? {id(c4) == id(c5)}"
    )
    print(
        f"{id(c5)} == {id(c6)}? {id(c5) == id(c6)}"
    )
        下面是完整代码列表(`ch04/flyweight.py`文件)的回顾,以展示如何实现和使用轻量级模式:

            1.  我们需要导入几个模块:`random`和`Enum`(来自`enum`模块)。

            1.  我们为汽车类型定义了`Enum`。

            1.  然后我们有`Car`类,它具有`pool`属性以及`__new__()`和`render()`方法。

            1.  在`main`函数的第一部分,我们定义了一些变量并渲染了一组小型车。

            1.  `main`函数的第二部分。

            1.  `main`函数的第三部分。

            1.  最后,我们添加`main`函数的第四部分。

        执行`python ch04/flyweight.py`命令的输出显示了渲染对象的类型、随机颜色和坐标,以及相同/不同家族的轻量级对象之间的身份比较结果:
render a gray SUBCOMPACT car at (25, 79)
render a black SUBCOMPACT car at (31, 99)
render a brown SUBCOMPACT car at (16, 74)
render a green SUBCOMPACT car at (10, 1)
render a gray SUBCOMPACT car at (55, 38)
render a red SUBCOMPACT car at (30, 45)
render a brown SUBCOMPACT car at (17, 78)
render a gray SUBCOMPACT car at (14, 21)
render a gray SUBCOMPACT car at (7, 28)
render a gray SUBCOMPACT car at (22, 50)
render a brown COMPACT car at (75, 26)
render a red COMPACT car at (22, 61)
render a white COMPACT car at (67, 87)
render a beige SUV car at (23, 93)
render a white SUV car at (37, 100)
render a red SUV car at (33, 98)
render a black SUV car at (77, 22)
render a green SUV car at (16, 51)
cars rendered: 18
cars actually created: 3
4493672400 == 4493672400? True
4493672400 == 4493457488? False
        由于颜色和坐标是随机的,并且对象身份取决于内存映射,因此不要期望看到相同的输出。

        代理模式

        **代理**设计模式的名称来源于用于在访问实际对象之前执行重要操作的**代理**对象(也称为**代表**)。有四种著名的代理类型。具体如下:

            1.  一个**虚拟代理**,它使用**延迟初始化**来推迟在真正需要时创建计算密集型对象。

            1.  一个**保护/防护代理**,用于控制对敏感对象的访问。

            1.  一个**远程代理**,作为实际存在于不同地址空间中的对象的本地表示(例如,网络服务器)。

            1.  一个**智能(引用)代理**,在访问对象时执行额外操作。此类操作的例子包括引用计数和线程安全检查。

        现实世界例子

        **芯片**卡是保护代理在现实生活中应用的一个好例子。借记/信用卡包含一个芯片,首先需要由 ATM 或读卡器读取。芯片验证后,需要输入密码(PIN)才能完成交易。这意味着,如果没有物理出示卡片并知道 PIN,就无法进行任何交易。

        使用现金购买和交易的银行支票是远程代理的一个例子。支票可以访问银行账户。

        在软件中,Python 的`weakref`模块包含一个`proxy()`方法,它接受一个输入对象并返回一个智能代理。弱引用是向对象添加引用计数支持的推荐方式。

        代理模式的用例

        由于至少有四种常见的代理类型,因此代理设计模式有许多用例。

        当使用私有网络或云来创建分布式系统时,这种模式被使用。在分布式系统中,一些对象存在于本地内存中,而一些对象存在于远程计算机的内存中。如果我们不希望客户端代码意识到这些差异,我们可以创建一个远程代理来隐藏/封装它们,使应用程序的分布式特性变得透明。

        当我们的应用程序由于昂贵对象的早期创建而遭受性能问题时,代理模式也很有用。通过使用虚拟代理进行延迟初始化,只在需要时创建对象,可以给我们带来显著的性能提升。

        作为第三个案例,这种模式用于检查用户是否有足够的权限访问某些信息。如果我们的应用程序处理敏感信息(例如,医疗数据),我们希望确保尝试访问/修改它的用户能够这样做。保护/防护代理可以处理所有与安全相关的操作。

        这种模式适用于我们的应用程序(或库、工具包、框架等)使用多个线程,并且我们希望将线程安全的问题从客户端代码转移到应用程序上。在这种情况下,我们可以创建一个智能代理来隐藏线程安全的复杂性,不让客户端知道。

        **对象关系映射**(ORM)API 也是如何使用远程代理的一个例子。许多流行的 Web 框架(Django、Flask、FastAPI...)使用 ORM 来提供面向对象的数据库访问。ORM 充当一个代理,可以位于任何地方,无论是本地服务器还是远程服务器。

        实现代理模式——虚拟代理

        在 Python 中创建虚拟代理有许多方法,但我总是喜欢关注惯用/Pythonic 的实现。这里展示的代码基于[stackoverflow.com](http://stackoverflow.com)网站的用户 Cyclone 给出的一个关于“Python memoising/deferred lookup property decorator”问题的优秀答案。

        注意

        在本节中,术语*属性*、*变量*和*属性*可以互换使用。

        首先,我们创建了一个`LazyProperty`类,它可以作为装饰器使用。当它装饰一个属性时,`LazyProperty`会在第一次使用时延迟加载该属性,而不是立即加载。`__init__()`方法创建了两个变量,用作初始化属性的方法的别名:`method`是实际方法的别名,`method_name`是方法名的别名。为了更好地理解这两个别名是如何使用的,将它们的值打印到输出中(取消注释代码中该部分的两个注释行):
class LazyProperty:
    def __init__(self, method):
        self.method = method
        self.method_name = method.__name__
        # print(f"function overriden: {self.method}")
        # print(f"function's name: {self.method_name}")
        `LazyProperty`类实际上是一个描述符。描述符是在 Python 中用来覆盖其属性访问方法(`__get__()`、`__set__()`和`__delete__()`)默认行为的推荐机制。`LazyProperty`类仅覆盖`__set__()`,因为这是它需要覆盖的唯一访问方法。换句话说,我们不需要覆盖所有访问方法。`__get__()`方法访问底层方法想要分配的属性值,并使用`setattr()`手动进行分配。`__get()__`实际上执行的操作非常巧妙:它用值替换了方法!这意味着属性不仅被延迟加载,而且只能设置一次。我们稍后会看到这意味着什么。
    def __get__(self, obj, cls):
        if not obj:
            return None
        value = self.method(obj)
        # print(f'value {value}')
        setattr(obj, self.method_name, value)
        return value
        再次,取消注释代码中该部分的注释行以获取一些额外信息。

        然后,`Test`类展示了我们如何使用`LazyProperty`类。有三个属性:`x`、`y`和`_resource`。我们希望`_resource`变量能够延迟加载;因此,我们将其初始化为`None`,如下所示:
class Test:
    def __init__(self):
        self.x = "foo"
        self.y = "bar"
        self._resource = None
        `resource()`方法被`LazyProperty`类装饰。为了演示目的,`LazyProperty`类将`_resource`属性初始化为一个元组,如下所示。通常,这会是一个缓慢/昂贵的初始化(数据库、图形等)。
    @LazyProperty
    def resource(self):
        print("initializing self._resource...")
        print(f"... which is: {self._resource}")
        self._resource = tuple(range(5))
        return self._resource
        如下所示的`main()`函数展示了延迟初始化的行为:
def main():
    t = Test()
    print(t.x)
    print(t.y)
    # do more work...
    print(t.resource)
    print(t.resource)
        注意,覆盖`__get()__`访问方法使得将`resource()`方法视为一个简单属性成为可能(我们可以使用`t.resource`而不是`t.resource()`)。

        让我们回顾一下示例代码(在`ch04/proxy/proxy_lazy.py`中):

            1.  我们定义了`LazyProperty`类。

            1.  我们定义了带有`resource()`方法的`Test`类,并使用`LazyProperty`对其进行装饰。

            1.  我们添加了主函数来测试我们的设计示例。

        如果你能够执行示例的原始版本(其中为了更好地理解而添加的行被注释),使用`python ch04/proxy/proxy_lazy.py`命令,你将得到以下输出:
foo
bar
initializing self._resource...
... which is: None
(0, 1, 2, 3, 4)
(0, 1, 2, 3, 4)
        根据这个输出,我们可以看到以下内容:

            +   `_resource`变量确实是在我们使用`t.resource`时初始化的,而不是在`t`实例创建时。

            +   第二次使用 `t.resource` 时,变量不再重新初始化。这就是为什么初始化字符串只初始化 `self._resource` 一次的原因。

        其他信息

        在面向对象编程(OOP)中,存在两种基本的懒加载初始化方式。具体如下:

        - **在实例级别**:这意味着对象的属性是懒加载初始化的,但属性具有对象作用域。同一类的每个实例(对象)都有自己的(不同的)属性副本。

        - **在类或模块级别**:在这种情况下,我们不希望每个实例有不同的副本,而是所有实例共享相同的属性,该属性是懒加载初始化的。这种情况在本章中没有涉及。如果你对此感兴趣,可以考虑将其作为练习。

        由于使用代理模式的可能性有很多,让我们看看另一个例子。

        实现代理模式 – 保护代理

        作为第二个例子,让我们实现一个简单的保护代理来查看和添加用户。服务提供了两种选项:

            +   **查看用户列表**:此操作不需要特殊权限

            +   **添加新用户**:此操作要求客户端提供特殊秘密消息

        `SensitiveInfo` 类包含我们想要保护的信息。`users` 变量是现有用户列表。`read()` 方法打印用户列表。`add()` 方法将新用户添加到列表中。该类的代码如下:
class SensitiveInfo:
    def __init__(self):
        self.users = ["nick", "tom", "ben", "mike"]
    def read(self):
        nb = len(self.users)
        print(f"There are {nb} users: {' '.join(self.users)}")
    def add(self, user):
        self.users.append(user)
        print(f"Added user {user}")
        `Info` 类是 `SensitiveInfo` 的保护代理。秘密变量是客户端代码添加新用户所需知道/提供的消息。

        注意,这只是一个例子。在现实中,你永远不应该做以下事情:

            +   在源代码中存储密码

            +   以明文形式存储密码

            +   使用弱(例如,MD5)或自定义形式的加密

        在 `Info` 类中,如我们接下来看到的,`read()` 方法是对 `SensitiveInfo.read()` 的包装,而 `add()` 方法确保只有当客户端代码知道秘密消息时,才能添加新用户:
class Info:
    def __init__(self):
        self.protected = SensitiveInfo()
        self.secret = "0xdeadbeef"
    def read(self):
        self.protected.read()
    def add(self, user):
        sec = input("what is the secret? ")
        if sec == self.secret:
            self.protected.add(user)
        else:
            print("That's wrong!")
        `main()` 函数展示了客户端代码如何使用代理模式。客户端代码创建 `Info` 类的实例,并使用显示的菜单读取列表、添加新用户或退出应用程序。让我们考虑以下代码:
def main():
    info = Info()
    while True:
        print("1\. read list |==| 2\. add user |==| 3\. quit")
        key = input("choose option: ")
        if key == "1":
            info.read()
        elif key == "2":
            name = input("choose username: ")
            info.add(name)
        elif key == "3":
            exit()
        else:
            print(f"unknown option: {key}")
        让我们回顾一下完整的代码(`ch04/proxy/proxy_protection.py`):

            1.  首先,我们定义 `SensitiveInfo` 类。

            1.  然后,我们有 `Info` 类的代码。

            1.  最后,我们添加主函数以及我们的测试代码。

        我们可以在以下示例中看到程序执行 `python ch04/proxy/proxy_protection.py` 命令时的输出样本:
1\. read list |==| 2\. add user |==| 3\. quit
choose option: 1
There are 4 users: nick tom ben mike
1\. read list |==| 2\. add user |==| 3\. quit
choose option: 2
choose username: tom
what is the secret? 0xdeadbeef
Added user tom
1\. read list |==| 2\. add user |==| 3\. quit
choose option: 3
        你已经发现了可以解决以改进我们的保护代理示例的缺陷或缺失功能吗?以下是一些建议:

            +   这个示例有一个非常大的安全漏洞。没有任何东西阻止客户端代码通过直接创建`SensitiveInfo`的实例来绕过应用程序的安全。改进这个示例以防止这种情况。一种方法是通过使用`abc`模块禁止直接实例化`SensitiveInfo`。在这种情况下还需要进行哪些代码更改?

            +   一个基本的安全规则是,我们永远不应该存储明文密码。只要我们知道使用哪些库,安全地存储密码并不难。如果你对安全感兴趣,尝试实现一种安全的方式来外部存储秘密消息(例如,在文件或数据库中)。

            +   应用程序仅支持添加新用户,但关于删除现有用户怎么办?添加一个`remove()`方法。

        实现代理模式 – 远程代理

        想象我们正在构建一个文件管理系统,客户端可以在远程服务器上执行文件操作。这些操作可能包括读取文件、写入文件和删除文件。远程代理隐藏了网络请求的复杂性,对客户端来说。

        我们首先创建一个接口,定义可以在远程服务器上执行的操作,`RemoteServiceInterface`,以及实现该接口的类`RemoteService`以提供实际服务。

        接口定义如下:
from abc import ABC, abstractmethod
class RemoteServiceInterface(ABC):
    @abstractmethod
    def read_file(self, file_name):
        pass
    @abstractmethod
    def write_file(self, file_name, contents):
        pass
    @abstractmethod
    def delete_file(self, file_name):
        pass
        `RemoteService`类定义如下(为了简单起见,方法仅返回一个字符串,但通常,你会在远程服务上进行特定的文件处理代码):
class RemoteService(RemoteServiceInterface):
    def read_file(self, file_name):
        # Implementation for reading a file from the server
        return "Reading file from remote server"
    def write_file(self, file_name, contents):
        # Implementation for writing to a file on the server
        return "Writing to file on remote server"
    def delete_file(self, file_name):
        # Implementation for deleting a file from the server
        return "Deleting file from remote server"
        然后,我们为代理定义了`ProxyService`。它实现了`RemoteServiceInterface`接口,并作为`RemoteService`的代理,处理与后者的通信:
class ProxyService(RemoteServiceInterface):
    def __init__(self):
        self.remote_service = RemoteService()
    def read_file(self, file_name):
        print("Proxy: Forwarding read request to RemoteService")
        return self.remote_service.read_file(file_name)
    def write_file(self, file_name, contents):
        print("Proxy: Forwarding write request to RemoteService")
        return self.remote_service.write_file(file_name, contents)
    def delete_file(self, file_name):
        print("Proxy: Forwarding delete request to RemoteService")
        return self.remote_service.delete_file(file_name)
        客户端与`ProxyService`组件交互,就像它是`RemoteService`一样,并不知道实际服务的远程性质。代理处理与远程服务的通信,可能包括添加日志、访问控制或缓存。为了测试,我们可以添加以下代码,基于创建`ProxyService`的实例:
if __name__ == "__main__":
    proxy = ProxyService()
    print(proxy.read_file("example.txt"))
        让我们回顾一下实现过程(完整代码位于`ch04/proxy/proxy_remote.py`):

            1.  我们首先定义接口,`RemoteServiceInterface`,以及一个实现该接口的类,`RemoteService`。

            1.  然后,我们定义了`ProxyService`类,它也实现了`RemoteService`接口。

            1.  最后,我们添加一些代码来测试代理对象。

        通过运行`python ch04/proxy/proxy_remote.py`,让我们看看示例的结果:
Proxy: Forwarding read request to RemoteService
Reading file from remote server
        这成功了。这个轻量级的示例有效地展示了如何实现远程代理用例。

        实现代理模式 – 智能代理

        让我们考虑一个场景,在你的应用程序中有一个共享资源,例如数据库连接。每次对象访问这个资源时,你都想跟踪资源引用的数量。一旦没有更多引用,资源就可以安全地释放或关闭。智能代理将帮助管理这个数据库连接的引用计数,确保它只在所有引用释放后关闭。

        在上一个示例中,我们需要一个定义访问数据库操作的接口,`DBConnectionInterface`,以及一个代表实际数据库连接的类,`DBConnection`。

        对于接口,我们使用`Protocol`(从`ABC`方式更改):
from typing import Protocol
class DBConnectionInterface(Protocol):
    def exec_query(self, query):
        ...
        数据库连接的类如下:
class DBConnection:
    def __init__(self):
        print("DB connection created")
    def exec_query(self, query):
        return f"Executing query: {query}"
    def close(self):
        print("DB connection closed")
        然后,我们定义了`SmartProxy`类;它也实现了`DBConnectionInterface`接口(请参阅`exec_query()`方法)。我们使用这个类来管理引用计数和`DBConnection`对象的访问。它确保在首次执行查询时按需创建`DBConnection`对象,并且只有当没有更多引用时才关闭。代码如下:
class SmartProxy:
    def __init__(self):
        self.cnx = None
        self.ref_count = 0
    def access_resource(self):
        if self.cnx is None:
            self.cnx = DBConnection()
        self.ref_count += 1
        print(f"DB connection now has {self.ref_count} references.")
    def exec_query(self, query):
        if self.cnx is None:
            # Ensure the connection is created
            # if not already
            self.access_resource()
        result = self.cnx.exec_query(query)
        print(result)
        # Decrement reference count after
        # executing query
        self.release_resource()
        return result
    def release_resource(self):
        if self.ref_count > 0:
            self.ref_count -= 1
            print("Reference released...")
            print(f"{self.ref_count} remaining refs.")
        if self.ref_count == 0 and self.cnx is not None:
            self.cnx.close()
            self.cnx = None
        现在,我们可以添加一些代码来测试实现:
if __name__ == "__main__":
    proxy = SmartProxy()
    proxy.exec_query("SELECT * FROM users")
    proxy.exec_query("UPDATE users SET name = 'John Doe' WHERE id = 1")
        让我们回顾一下实现(完整代码在`ch04/proxy/proxy_smart.py`中):

            1.  我们首先定义接口,`DBConnectionInterface`,以及一个实现它的类,代表数据库连接,`DBConnection`。

            1.  然后,我们定义了`SmartProxy`类,它也实现了`DBConnectionInterface`。

            1.  最后,我们添加一些代码来测试代理对象。

        让我们通过运行`python ch04/proxy/proxy_smart.py`来查看示例的结果:
DB connection created
DB connection now has 1 references.
Executing query: SELECT * FROM users
Reference released...
0 remaining refs.
DB connection closed
DB connection created
DB connection now has 1 references.
Executing query: UPDATE users SET name = 'John Doe' WHERE id = 1
Reference released...
0 remaining refs.
DB connection closed
        这是对代理模式的一次另类演示。在这里,它帮助我们实现了一种改进的解决方案,适用于数据库连接在不同应用程序部分之间共享且需要谨慎管理以避免耗尽数据库资源或泄露连接的场景。

        摘要

        结构模式对于创建干净、可维护和可扩展的代码至关重要。它们为你在日常编码中遇到的许多挑战提供了解决方案。

        首先,适配器模式作为一种灵活的解决方案,用于协调不匹配的接口。我们可以使用这种模式来弥合过时遗留系统与现代接口之间的差距,从而促进更加紧密和易于管理的软件系统。

        然后,我们讨论了装饰器模式,这是一种方便地扩展对象行为而不使用继承的方法。Python 通过其内置的装饰器功能,甚至进一步扩展了装饰器概念,允许我们扩展任何可调用的行为而不使用继承或组合。装饰器模式是实现横切关注点的绝佳解决方案,因为它们是通用的,并且不适合 OOP 范式。我们在“装饰器模式的使用案例”部分提到了几个横切关注点的类别。我们看到了装饰器如何帮助我们保持函数的整洁,同时不牺牲性能。

        与适配器模式相似,桥接模式在定义抽象及其实现时有所不同,它是在一开始就以一种解耦的方式定义抽象和实现,以便两者可以独立变化。桥接模式在编写操作系统和设备驱动程序、GUI 和网站构建器等问题的软件时非常有用,在这些领域中我们有多套主题,并且需要根据某些属性更改网站的主题。我们在内容提取和管理领域讨论了一个例子,其中我们定义了一个抽象的接口、一个实现者的接口和两个实现。

        外观模式非常适合为希望使用复杂系统但不需要了解系统复杂性的客户端代码提供一个简单的接口。计算机就是一个外观,因为我们只需要按下一个按钮就可以打开它。所有其他硬件复杂性都由 BIOS、引导加载程序和系统软件的其他组件透明地处理。还有更多现实生活中的外观例子,比如当我们连接到银行或公司的客户服务部门时,以及我们用来启动车辆的钥匙。我们介绍了一个多服务器操作系统使用的接口实现。

        通常,当应用程序需要创建大量计算成本高昂的对象,而这些对象共享许多属性时,我们会使用享元模式。关键点是区分不可变(共享)属性和可变属性。我们看到了如何实现一个支持三个不同汽车家族的汽车渲染器。通过显式地将可变的颜色和 x、y 属性提供给`render()`方法,我们只创建了 3 个不同的对象,而不是 18 个。虽然这可能看起来不是很大的胜利,但想象一下如果汽车有 2,000 辆而不是 18 辆会怎样。

        我们以代理模式结束。我们讨论了代理模式的几个用例,包括性能、安全性和如何向用户提供简单的 API。我们为通常需要的四种代理类型中的每一种都看到了实现示例:虚拟代理、保护代理、远程服务代理和智能代理。

        在下一章中,我们将探讨行为设计模式,这些模式涉及对象交互和算法。

第五章:行为设计模式

在上一章中,我们介绍了结构化模式以及帮助我们创建干净、可维护和可扩展代码的面向对象编程OOP)模式。下一个设计模式类别是行为设计模式。行为模式处理对象之间的连接和算法。

在本章中,我们将涵盖以下主要主题:

  • 责任链模式

  • 命令模式

  • 观察者模式

  • 状态模式

  • 解释器模式

  • 策略模式

  • 备忘录模式

  • 迭代器模式

  • 模板模式

  • 其他行为设计模式

在本章结束时,你将了解如何使用行为模式来改进你的软件项目设计。

技术要求

请参阅在第一章中提出的各项要求。本章讨论的代码的附加技术要求如下:

  • 对于状态模式部分,请使用以下命令安装state_machine模块:python -m pip install state_machine

  • 对于解释器模式部分,请使用以下命令安装pyparsing模块:python –m pip install pyparsing

  • 对于模板模式部分,请使用以下命令安装cowpy模块:python -m pip install cowpy

责任链模式

责任链模式提供了一种优雅的方式来处理请求,通过将它们传递给一系列处理者。链中的每个处理者都有自主权来决定它是否可以处理请求,或者是否应该将其委托给链中的其他处理者。当处理涉及多个处理者但不必所有处理者都参与的操作时,此模式特别有用。

在实践中,此模式鼓励我们关注对象以及请求在应用程序中的流动。值得注意的是,客户端代码对整个处理者链一无所知。相反,它只与链中的第一个处理元素交互。同样,每个处理元素只知道其直接的后继者,形成一个类似于单链表的单向关系。这种结构是故意设计的,旨在在发送者(客户端)和接收者(处理元素)之间实现解耦。

现实世界示例

自动柜员机(ATM)以及任何接受/退还纸币或硬币的机器(例如,零食自动售货机)都使用责任链模式。所有纸币都有一个单独的槽位,如下面的图所示,由www.sourcemaking.com提供:

图 5.1 – 责任链模式示例:ATM

图 5.1 – 责任链模式示例:ATM

当纸币被投入时,它会被路由到相应的容器。当它被退回时,它会被从相应的容器中取出。我们可以将单个槽位视为共享通信介质,而不同的容器则视为处理元素。结果包含来自一个或多个容器的现金。例如,在前面的图中,我们看到当我们从 ATM 请求 175 美元时会发生什么。

在某些 Web 框架中,过滤器或中间件是在 HTTP 请求到达目标之前执行的代码片段。存在一个过滤器链。每个过滤器执行不同的操作(用户认证、日志记录、数据压缩等),要么将请求转发到下一个过滤器,直到链被耗尽,要么在出现错误时(例如,认证失败三次)中断流程。

责任链模式的用例

通过使用责任链模式,我们为多个不同的对象提供了一个满足特定请求的机会。当我们事先不知道哪个对象应该满足给定的请求时,这非常有用。一个例子是采购系统。在采购系统中,有许多审批权限。一个审批权限可能能够批准价值达到一定数额的订单,比如说 100 美元。如果订单价值超过 100 美元,订单就会被发送到链中的下一个审批权限,它可以批准价值达到 200 美元的订单,以此类推。

责任链模式在另一种情况下也很有用,即我们知道可能需要多个对象来处理单个请求。这就是基于事件编程中发生的情况。一个事件,比如左键点击,可以被多个监听器捕获。

需要注意的是,如果所有请求都可以由单个处理元素处理,除非我们真的不知道是哪个元素,否则责任链模式并不是非常有用。这种模式的价值在于它提供的解耦,正如我们在第一章松耦合部分中看到的,基础设计原则。与客户端和所有处理元素之间以及处理元素和所有其他处理元素之间的一对多关系相比,客户端只需要知道如何与链的起始(头部)进行通信。

实现责任链模式

在 Python 中实现责任链模式有许多方法,但我最喜欢的实现是 Vespe Savikko 的版本(legacy.python.org/workshops/1997-10/proceedings/savikko.html)。Vespe 的实现使用 Python 风格的动态分派来处理请求。

让我们以 Vespe 的实现为指导,实现一个简单的基于事件的系统。以下是该系统的 UML 类图:

图 5.2 – 基于事件的窗口系统的 UML 类图

图 5.2 – 基于事件的窗口系统的 UML 类图

Event类描述了一个事件。我们将保持简单,因此,在我们的例子中,一个事件只有一个name

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

Widget类是应用程序的核心类。按照惯例,我们假设parent对象是一个Widget实例。然而,请注意,根据继承规则,Widget的任何子类(例如,MsgText的实例)也是一个Widget实例。该类有一个handle()方法,它通过hasattr()getattr()进行动态分发来决定特定请求(事件)的处理者。如果请求处理事件的窗口不支持该事件,有两种回退机制。如果窗口有父窗口,则执行父窗口的handle()方法。如果没有父窗口但有handle_default()方法,则执行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()

MainWIndowMsgTextSendDialog都是具有不同行为的窗口。并不期望这三个窗口都能处理相同的事件,即使它们可以处理相同的事件,它们的行为也可能不同。MainWindow只能处理关闭和默认事件:

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()函数展示了如何创建一些窗口和事件,以及窗口如何对这些事件做出反应。所有事件都发送到所有窗口。注意每个窗口的父关系——sd对象(SendDialog的实例)的父对象是mw对象(MainWindow的实例)。然而,并非所有对象都需要有一个是MainWindow实例的父对象。例如,msg对象(MsgText的实例)的父对象是sd对象:

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):

  1. 我们定义了Event类,然后是Widget类。

  2. 我们定义了专门的窗口类,MainWindowSendDialogMsgText

  3. 最后,我们添加了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

命令模式

现在大多数应用程序都有撤销操作。这很难想象,但在许多年之前,没有任何软件有撤销功能。撤销功能是在 1974 年引入的,但 Fortran 和 Lisp 这两种至今仍被广泛使用的编程语言分别在 1957 年和 1958 年创建!我不愿意在那几年成为应用程序用户。犯错意味着用户没有简单的方法来修复它。

足够的历史了。我们想知道如何在我们的应用程序中实现撤销功能。既然你已经阅读了本章的标题,你就已经知道推荐使用哪种设计模式来实现撤销:命令模式。

命令设计模式帮助我们封装一个操作(撤销、重做、复制、粘贴等等)作为一个对象。这意味着我们创建一个包含所有逻辑和实现操作所需的方法的类。这样做的好处如下:

  • 我们不必直接执行命令。它可以在任何时候执行。

  • 调用命令的对象与知道如何执行它的对象解耦。调用者不需要了解命令的任何实现细节。

  • 如果有道理,可以将多个命令分组,以便调用者可以按顺序执行它们。这在实现多级撤销命令时很有用。

现实世界的例子

当我们去餐馆吃晚餐时,我们会向服务员下单。他们用来写下订单的账单(通常是纸张)是一个命令的例子。写下订单后,服务员将其放入由厨师执行的账单队列中。每一张账单都是独立的,可以用来执行许多不同的命令,例如,为每一项将要烹饪的菜品执行一个命令。

如您所预期的那样,我们也在软件中找到了几个例子。以下是我能想到的两个:

  • PyQt 是 QT 工具包的 Python 绑定。PyQt 包含一个QAction类,它将操作建模为命令。每个动作都支持额外的可选信息,例如描述、工具提示和快捷键。

  • Git Cola,一个用 Python 编写的 Git GUI,使用命令模式来修改模型、修正提交、应用不同的选举、检出等等。

命令模式的用例

许多开发者将撤销示例作为命令模式的唯一用例。事实是,撤销是命令模式的杀手级功能。然而,命令模式实际上可以做更多:

  • GUI 按钮和菜单项:之前提到的 PyQt 示例使用命令模式来实现按钮和菜单项上的操作。

  • 其他操作:除了撤销之外,命令还可以用来实现任何操作。一些例子包括剪切复制粘贴重做首字母大写文本

  • 事务行为和日志记录:事务行为和日志记录对于保持更改的持久日志非常重要。操作系统使用它们从系统崩溃中恢复,关系数据库使用它们实现事务,文件系统使用它们实现快照,安装程序(向导)使用它们撤销已取消的安装。

  • :在这里,我们指的是可以记录并在任何时间点按需执行的动作序列。流行的编辑器,如 Emacs 和 Vim,支持宏。

实现命令模式

让我们使用命令模式来实现以下基本文件实用工具:

  • 创建文件,可选地添加文本到其中

  • 读取文件的内容

  • 重命名文件

我们不会从头开始实现这些实用工具,因为 Python 已经在os模块中提供了良好的实现。我们想要做的是在这些实用工具之上添加一个额外的抽象层,使它们可以被当作命令处理。通过这样做,我们获得了命令提供的所有优势。

每个命令都有两个部分:

  • __init__()方法包含命令执行有用操作所需的所有信息(文件的路径、将要写入文件的内容等等)。

  • execute()方法。当我们想要运行一个命令时,我们会调用该方法。这并不一定是在初始化之后立即进行的。

让我们从重命名实用工具开始,我们使用RenameFile类来实现它。该类使用源文件路径和目标文件路径进行初始化。我们添加了execute()方法,它使用os.rename()执行实际的重命名操作。为了提供撤销操作的支持,我们添加了undo()方法,在其中我们再次使用os.rename()将文件的名称恢复到原始值。请注意,我们还使用了日志记录来改进输出。

代码的开始部分,我们需要导入的内容,以及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”文本。通常,合理的默认行为是创建一个空文件,但为了本例的需求,我决定在其中写入一个默认字符串。然后,我们添加一个execute()方法,在其中我们使用 Python 的open()函数以write()模式打开文件,并将txt字符串写入其中。

创建文件操作的撤销是删除该文件。因此,我们在类中添加了undo()方法,其中我们使用os.remove()函数来完成这项工作。

CreateFile类的定义如下所示:

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()方法再次使用open(),这次是在读取模式下,并仅打印文件的内容。

ReadFile 类定义如下:

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 参数是创建并重命名文件的原始名称和新名称。使用命令列表来添加(并配置)我们希望在以后执行的命令。代码如下:

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()

然后,我们询问用户是否想要撤销已执行的命令。用户选择是否撤销命令。如果他们选择撤销,则对命令列表中的所有命令执行 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 文件中):

  1. 我们导入 loggingos 模块。

  2. 我们进行常规的日志配置。

  3. 我们定义了 RenameFile 类。

  4. 我们定义了 CreateFile 类。

  5. 我们定义了 ReadFile 类。

  6. 我们添加了一个 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
ERROR, in the first case, is normal for this context.
			The Observer pattern
			The Observer pattern describes a publish-subscribe relationship between a single object, the publisher, which is also known as the subject or **observable**, and one or more objects, the subscribers, also known as **observers**. So, the subject notifies the subscribers of any state changes, typically by calling one of their methods.
			The ideas behind the Observer pattern are the same as those behind the separation of concerns principle, that is, to increase decoupling between the publisher and subscribers, and to make it easy to add/remove subscribers at runtime.
			Real-world examples
			Dynamics in an auction are similar to the behavior of the Observer pattern. Every auction bidder has a number paddle that is raised whenever they want to place a bid. Whenever the paddle is raised by a bidder, the auctioneer acts as the subject by updating the price of the bid and broadcasting the new price to all bidders (subscribers).
			In software, we can cite at least two examples:

				*   **Kivy**, the Python framework for developing **user interfaces** (**UIs**), has a module called **Properties**, which implements the Observer pattern. Using this technique, you can specify what should happen when a property’s value changes.
				*   The **RabbitMQ library** provides an implementation of an **Advanced Message Queuing Protocol** (**AMQP**) messaging broker. It is possible to construct a Python application that interacts with RabbitMQ in such a way that it subscribes to messages and publishes them to queues, which is essentially the Observer design pattern.

			Use cases for the Observer pattern
			We generally use the Observer pattern when we want to inform/update one or more objects (observers/subscribers) about a change that happened on a given object (subject/publisher/observable). The number of observers, as well as who those observers are, may vary and can be changed dynamically.
			We can think of many cases where Observer can be useful. One such use case is news feeds. With RSS, Atom, or other related formats, you follow a feed, and every time it is updated, you receive a notification about the update.
			The same concept exists in social networking applications. If you are connected to another person using a social networking service, and your connection updates something, you are notified about it.
			Event-driven systems are another example where Observer is usually used. In such systems, you have listeners that listen for specific events. The listeners are triggered when an event they are listening to is created. This can be typing a specific key (on the keyboard), moving the mouse, and more. The event plays the role of the *publisher*, and the listeners play the role of the *observers*. The key point in this case is that multiple listeners (observers) can be attached to a single event (publisher).
			Implementing the Observer pattern
			As an example, let’s implement a weather monitoring system. In such a system, you have a weather station that collects weather-related data (temperature, humidity, and atmospheric pressure). Our system needs to allow different devices and applications to receive real-time updates whenever there is a change in the weather data.
			We can apply the Observer pattern using the following elements:

				*   `WeatherStation` class that acts as the subject. This class will maintain a list of observers (devices or applications) interested in receiving weather updates.
				*   **Observers (devices and applications)**: Implement various observer classes, representing devices such as smartphones, tablets, weather apps, and even a display screen in a local store. Each observer will subscribe to receive updates from the weather station.
				*   `subscribe`) and unregister (`unsubscribe`) themselves. When there is a change in weather data (e.g., a new temperature reading), the weather station notifies all registered observers.
				*   `update()` method that the weather station calls when notifying about changes. For instance, a smartphone observer may update its weather app with the latest data, while a local store display may update its digital sign.

			Let’s get started.
			First, we define the `Observer` interface, which holds an `update` method that observers must implement. Observers are expected to update themselves when the subject’s state changes:

class Observer:

def update(self, temperature, humidity, pressure):

pass


			Next, we define the `WeatherStation` subject class. It maintains a list of observers and provides methods to add and remove observers. The `set_weather_data` method is used to simulate changes in weather data. When the weather data changes, it notifies all registered observers by calling their `update` methods. The code is as follows:

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)


			Let’s now define the `DisplayDevice` observer class. Its `update` method prints weather information when called:

class DisplayDevice(Observer):

def init(self, name):

self.name = name

def update(self, temperature, humidity, pressure):

print(f"{self.name} 显示屏")

print(

f" - 温度:{temperature}°C, 湿度:{humidity}%, 气压:{pressure}hPa"

)


			Similarly, we define another observer class, `WeatherApp`, which prints weather information in a different format when its `update` method is called:

class WeatherApp(Observer):

def init(self, name):

self.name = name

def update(self, temperature, humidity, pressure):

print(f"{self.name} 应用 - 天气更新")

print(

f" - 温度:{temperature}°C, 湿度:{humidity}%, 气压:{pressure}hPa"

)


			Now, in the `main()` function, we do several things:

				*   We create an instance of the `WeatherStation` class, which acts as the subject.
				*   We create instances of `DisplayDevice` and `WeatherApp`, representing different types of observers.
				*   We register these observers with `weather_station` using the `add_observer` method.
				*   We simulate changes in weather data by calling the `set_weather_data` method of `weather_station`. This triggers updates to all registered observers.

			The code of the `main()` function is as follows:

def main():

创建天气站

weather_station = WeatherStation()

创建并注册观察者

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)

模拟天气数据变化

weather_station.set_weather_data(25.5, 60, 1013.2)

weather_station.set_weather_data(26.0, 58, 1012.8)


			Let’s recapitulate the complete code (in the `ch05/observer.py` file) of the implementation:

				1.  We define the Observer interface.
				2.  We define the `WeatherStation` subject class.
				3.  We define two observer classes, `DisplayDevice` and `WeatherApp`.
				4.  We add a `main()` function where we test our design.

			Executing the `python ch05/observer.py` command gives us the following output:

客厅显示屏

  • 温度:25.5°C,湿度:60%,气压:1013.2hPa

卧室显示屏

  • 温度:25.5°C,湿度:60%,气压:1013.2hPa

移动应用应用 - 天气更新

  • 温度:25.5°C,湿度:60%,气压:1013.2hPa

客厅显示屏

  • 温度:26.0°C, 湿度:58%,气压:1012.8hPa

卧室显示屏

  • 温度:26.0°C, 湿度:58%,气压:1012.8hPa

移动应用应用 - 天气更新

  • 温度:26.0°C, 湿度:58%,气压:1012.8hPa

			As you can see, this example demonstrates the Observer pattern, where the subject notifies its observers about changes in its state. Observers are loosely coupled with the subject and can be added or removed dynamically, providing flexibility and decoupling in the system.
			As an exercise, you can see that when unregistering an observer, using the `remove_observer()` method, and then simulating additional weather data changes, only the remaining registered observers receive updates. As a helper, to test this, here are 2 lines of code to add at the end of the `main()` function:

weather_station.remove_observer(display2)

weather_station.set_weather_data(27.2, 55, 1012.5)


			Next, we will discuss the State pattern.
			The State pattern
			In the previous chapter, we covered the Observer pattern, which is useful in a program to notify other objects when the state of a given object changes. Let’s continue discovering those patterns proposed by the Gang of Four.
			OOP focuses on maintaining the states of objects that interact with each other. A very handy tool to model state transitions when solving many problems is known as a **finite-state machine** (commonly called a **state machine**).
			What’s a state machine? A state machine is an abstract machine that has two key components, that is, states and transitions. A state is the current (active) status of a system. For example, if we have a radio receiver, two possible states for it are to be tuned to FM or AM. Another possible state is for it to be switching from one FM/AM radio station to another. A transition is a switch from one state to another. A transition is initiated by a triggering event or condition. Usually, an action or set of actions is executed before or after a transition occurs. Assuming that our radio receiver is tuned to the 107 FM station, an example of a transition is for the button to be pressed by the listener to switch it to 107.5 FM.
			A nice feature of state machines is that they can be represented as graphs (called **state diagrams**), where each state is a node, and each transition is an edge between two nodes.
			State machines can be used to solve many kinds of problems, both non-computational and computational. Non-computational examples include vending machines, elevators, traffic lights, combination locks, parking meters, and automated gas pumps. Computational examples include game programming and other categories of computer programming, hardware design, protocol design, and programming language parsing.
			Now, we have an idea of what state machines are! But how are state machines related to the State design pattern? It turns out that the State pattern is nothing more than a state machine applied to a particular software engineering problem (*Gang of Four-95*, page 342), (*Python 3 Patterns, Recipes and Idioms by Bruce Eckel & Friends*, page 151).
			Real-world examples
			A snack vending machine is an example of the State pattern in everyday life. Vending machines have different states and react differently depending on the amount of money that we insert. Depending on our selection and the money we insert, the machine can do the following:

				*   Reject our selection because the product we requested is out of stock.
				*   Reject our selection because the amount of money we inserted was not sufficient.
				*   Deliver the product and give no change because we inserted the exact amount.
				*   Deliver the product and return the change.

			There are, for sure, more possible states, but you get the point.
			Other examples of the state pattern in real life are as follows:

				*   Traffic lights
				*   Game states in a video game

			In software, the state pattern is commonly used. Python and its ecosystem offer several packages/modules one can use to implement state machines. We will see how to use one of them in the implementation section.
			Use cases for the State pattern
			The State pattern is applicable to many problems. All the problems that can be solved using state machines are good use cases for using the State pattern. An example we have already seen is the process model for an operating/embedded system.
			Programming language compiler implementation is another good example. Lexical and syntactic analysis can use states to build abstract syntax trees.
			Event-driven systems are yet another example. In an event-driven system, the transition from one state to another triggers an event/message. Many computer games use this technique. For example, a monster might move from the guard state to the attack state when the main hero approaches it.
			To quote Thomas Jaeger, in his article, *The State Design Pattern vs. State* *Machine* ([`thomasjaeger.wordpress.com/2012/12/13/the-state-design-pattern-vs-state-machine-2/`](https://thomasjaeger.wordpress.com/2012/12/13/the-state-design-pattern-vs-state-machine-2/)):
			*The state design pattern allows for full encapsulation of an unlimited number of states on a context for easy maintenance* *and flexibility.*
			Implementing the State pattern
			Let’s write code that demonstrates how to create a state machine based on the state diagram shown earlier in this chapter. Our state machine should cover the different states of a process and the transitions between them.
			The State design pattern is usually implemented using a parent `State` class that contains the common functionality of all the states, and several concrete classes derived from `State`, where each derived class contains only the state-specific required functionality. The State pattern focuses on implementing a state machine. The core parts of a state machine are the states and transitions between the states. It doesn’t matter how those parts are implemented.
			To avoid reinventing the wheel, we can make use of existing Python modules that not only help us create state machines but also do it in a Pythonic way. A module that I find very useful is `state_machine`.
			The `state_machine` module is simple enough that no special introduction is required. We will cover most aspects of it while going through the code of the example.
			Let’s start with the `Process` class. Each created process has its own state machine. The first step to creating a state machine using the `state_machine` module is to use the `@acts_as_state_machine` decorator. Then, we define the states of our state machine. This is a one-to-one mapping of what we see in the state diagram. The only difference is that we should give a hint about the initial state of the state machine. We do that by setting the initial attribute value to `True`:

@acts_as_state_machine

class Process:

created = 状态(initial=True)

waiting = 状态()

运行中 = 状态()

终止 = 状态()

blocked = 状态()

swapped_out_waiting = 状态()

swapped_out_blocked = 状态()


			Next, we are going to define the transitions. In the `state_machine` module, a transition is an instance of the `Event` class. We define the possible transitions using the `from_states` and `to_state` arguments:

wait = Event(

from_states=(

created,

运行中,

blocked,

swapped_out_waiting,

),

to_state=等待,

)

run = Event(

from_states=等待, to_state=运行中

)

terminate = Event(

from_states=运行中, to_state=终止

)

block = Event(

from_states=(

运行中,

swapped_out_blocked,

),

to_state=阻塞,

)

swap_wait = Event(

from_states=等待,

to_state=swapped_out_blocked,

)

swap_block = Event(

from_states=阻塞,

to_state=swapped_out_blocked,

)


			Also, as you may have noticed that `from_states` can be either a single state or a group of states (tuple).
			Each process has a name. Officially, a process needs to have much more information to be useful (for example, ID, priority, status, and so forth) but let’s keep it simple to focus on the pattern:

def init(self, name):

self.name = name


			Transitions are not very useful if nothing happens when they occur. The `state_machine` module provides us with the `@before` and `@after` decorators that can be used to execute actions before or after a transition occurs, respectively. You can imagine updating some objects within the system or sending an email or a notification to someone. For this example, the actions are limited to printing information about the state change of the process, as follows:

@after("wait")

def wait_info(self):

print(f"{self.name} 进入等待模式")

@after("run")

def run_info(self):

print(f"{self.name} 正在运行")

@before("terminate")

def terminate_info(self):

print(f"{self.name} 终止")

@after("block")

def block_info(self):

print(f"{self.name} 正在阻塞")

@after("swap_wait")

def swap_wait_info(self):

print(

f"{self.name} 被交换出并等待"

)

@after("swap_block")

def swap_block_info(self):

print(

f"{self.name} 被交换出并阻塞"

)


			Next, we need the `transition()` function, which accepts three arguments:

				*   `process`, which is an instance of `Process`
				*   `event`, which is an instance of `Event` (wait, run, terminate, and so forth)
				*   `event_name`, which is the name of the event

			The name of the event is printed if something goes wrong when trying to execute `event`. Here is the code for the function:

def transition(proc, event, event_name):

try:

event()

except InvalidStateTransition:

msg = (

f"过程 {proc.name} 从 {proc.current_state} "

f"转换到 {event_name} 失败"

)

print(msg)


			The `state_info()` function shows some basic information about the current (active) state of the process:

def state_info(proc):

print(

f"状态 {proc.name}: {proc.current_state}"

)


			At the beginning of the `main()` function, we define some string constants, which are passed as `event_name`:

def main():

RUNNING = "运行中"

WAITING = "等待"

BLOCKED = "阻塞"

TERMINATED = "终止"


			Next, we create two `Process` instances and display information about their initial state:

p1, p2 = Process("process1"), Process(

"process2"

)

[state_info(p) for p in (p1, p2)]


			The rest of the function experiments with different transitions. Recall the state diagram we covered in this chapter. The allowed transitions should be with respect to the state diagram. For example, it should be possible to switch from a running state to a blocked state, but it shouldn’t be possible to switch from a blocked state to a running state:

print()

transition(p1, p1.wait, WAITING)

transition(p2, p2.terminate, 终止)

[state_info(p) for p in (p1, p2)]

print()

transition(p1, p1.run, 运行中)

transition(p2, p2.wait, WAITING)

[state_info(p) for p in (p1, p2)]

print()

transition(p2, p2.run, 运行中)

[state_info(p) for p in (p1, p2)]

print()

[

transition(p, p.block, 阻塞)

for p in (p1, p2)

]

[state_info(p) for p in (p1, p2)]

print()

[

transition(p, p.terminate, 终止)

for p in (p1, p2)

]

[state_info(p) for p in (p1, p2)]


			Here is the recapitulation of the full implementation example (the `ch05/state.py` file):

				1.  We begin by importing what we need from `state_machine`.
				2.  We define the `Process` class with its simple attributes.
				3.  We add the `Process` class’s initialization method.
				4.  We also need to define, in the `Process` class, the methods to provide its states.
				5.  We define the `transition()` function.
				6.  Next, we define the `state_info()` function.
				7.  Finally, we add the main function of the program.

			Here’s what we get when executing the Python `ch05/state.py` command:

状态 1:已创建

状态 2:已创建

process1 进入等待模式

过程 2 从已创建到终止的转换失败

状态 1:等待

状态 2:已创建

process1 正在运行

process2 进入等待模式

状态 1:运行中

状态 2:等待

process2 正在运行

状态 1:运行中

状态 2:运行中

process1 正在阻塞

process2 正在阻塞

状态 1:阻塞

状态 2:阻塞

过程 1 从阻塞到终止的转换失败

process2 从阻塞到终止的转换失败

process1 的状态:阻塞

process2 的状态:阻塞


			Indeed, the output shows that illegal transitions such as created → terminated and blocked → terminated fail gracefully. We don’t want the application to crash when an illegal transition is requested, and this is handled properly by the except block.
			Notice how using a good module such as `state_machine` eliminates conditional logic. There’s no need to use long and error-prone `if…else` statements that check for each and every state transition and react to them.
			To get a better feeling for the state pattern and state machines, I strongly recommend you implement your own example. This can be anything: a simple video game (you can use state machines to handle the states of the main hero and the enemies), an elevator, a parser, or any other system that can be modeled using state machines.
			The Interpreter pattern
			Often, we need to create a **domain-specific language** (**DSL**). A DSL is a computer language of limited expressiveness targeting a particular domain. DSLs are used for different things, such as combat simulation, billing, visualization, configuration, and communication protocols. DSLs are divided into internal DSLs and external DSLs.
			Internal DSLs are built on top of a host programming language. An example of an internal DSL is a language that solves linear equations using Python. The advantages of using an internal DSL are that we don’t have to worry about creating, compiling, and parsing grammar because these are already taken care of by the host language. The disadvantage is that we are constrained by the features of the host language. It is very challenging to create an expressive, concise, and fluent internal DSL if the host language does not have these features.
			External DSLs do not depend on host languages. The creator of the DSL can decide all aspects of the language (grammar, syntax, and so forth). They are also responsible for creating a parser and compiler for it.
			The Interpreter pattern is related only to internal DSLs. Therefore, the goal is to create a simple but useful language using the features provided by the host programming language, which in this case is Python. Note that Interpreter does not address parsing at all. It assumes that we already have the parsed data in some convenient form. This can be an **abstract syntax tree** (**AST**) or any other handy data structure [*Gang of Four-95*, page 276].
			Real-world examples
			A musician is an example of the Interpreter pattern. Musical notation represents the pitch and duration of a sound graphically. The musician can reproduce a sound precisely based on its notation. In a sense, musical notation is the language of music, and the musician is the interpreter of that language.
			We can also cite software examples:

				*   In the C++ world, `boost::spirit` is considered an internal DSL for implementing parsers.
				*   An example in Python is PyT, an internal DSL used to generate XHTML/HTML. PyT focuses on performance and claims to have comparable speed with Jinja2\. Of course, we should not assume that the Interpreter pattern is necessarily used in PyT. However, since it is an internal DSL, the Interpreter is a very good candidate for it.

			Use cases for the Interpreter pattern
			The Interpreter pattern is used when we want to offer a simple language to domain experts and advanced users to solve their problems. The first thing we should stress is that the Interpreter pattern should only be used to implement simple languages. If the language has the requirements of an external DSL, there are better tools to create languages from scratch (Yacc and Lex, Bison, ANTLR, and so on).
			Our goal is to offer the right programming abstractions to the specialist, who is often not a programmer, to make them productive. Ideally, they shouldn’t know advanced Python to use our DSL, but knowing even a little bit of Python is a plus since that’s what we eventually get at the end. Advanced Python concepts should not be a requirement. Moreover, the performance of the DSL is usually not an important concern. The focus is on offering a language that hides the peculiarities of the host language and offers a more human-readable syntax. Admittedly, Python is already a very readable language with far less peculiar syntax than many other programming languages.
			Implementing the Interpreter pattern
			Let’s create an internal DSL to control a smart house. This example fits well into the **internet of things** (**IoT**) era, which is getting more and more attention nowadays. The user can control their home using a very simple event notation. An event has the form of command -> receiver -> arguments. The arguments part is optional.
			Not all events require arguments. An example of an event that does not require any arguments is shown here:

open -> gate


			An example of an event that requires arguments is shown here:

increase -> boiler temperature -> 3 degrees


			The `->` symbol is used to mark the end of one part of an event and state the beginning of the next one. There are many ways to implement an internal DSL. We can use plain old regular expressions, string processing, a combination of operator overloading, and metaprogramming, or a library/tool that can do the hard work for us. Although, officially, the Interpreter pattern does not address parsing, I feel that a practical example needs to cover parsing as well. For this reason, I decided to use a tool to take care of the parsing part. The tool is called pyparsing and, to find out more about it, check out the mini-book *Getting Started with Pyparsing* by Paul McGuire ([`www.oreilly.com/library/view/getting-started-with/9780596514235/`](https://www.oreilly.com/library/view/getting-started-with/9780596514235/)).
			Before getting into coding, it is a good practice to define a simple grammar for our language. We can define the grammar using the **Backus-Naur Form** (**BNF**) notation:

event ::= command token receiver token arguments

command ::= word+

word ::= 一组一个或多个字母数字字符

token ::= ->

receiver ::= word+

arguments ::= word+


			What the grammar basically tells us is that an event has the form of command -> receiver -> arguments, and that commands, receivers, and arguments have the same form: a group of one or more alphanumeric characters. If you are wondering about the necessity of the numeric part, it is included to allow us to pass arguments, such as three degrees at the increase -> boiler temperature -> 3 degrees command.
			Now that we have defined the grammar, we can move on to converting it to actual code. Here’s what the code looks like:

word = Word(alphanums)

command = Group(OneOrMore(word))

token = Suppre"s("->")

device = Group(OneOrMore(word))

argument = Group(OneOrMore(word))

event = command + token + device + Optional(token + argument)


			The basic difference between the code and grammar definition is that the code needs to be written in the bottom-up approach. For instance, we cannot use a word without first assigning it a value. `Suppress` is used to state that we want the `->` symbol to be skipped from the parsed results.
			The full code of the final implementation example (see the `ch05/interpreter/interpreter.py` file) uses many placeholder classes, but to keep you focused, I will first show a minimal version featuring only one class. Let’s look at the `Boiler` class. A boiler has a default temperature of 83° Celsius. There are also two methods to increase and decrease the current temperature:

class Boiler:

def init(self):

self.temperature = 83  # in celsius

def str(self):

return f"锅炉温度:{self.temperature}"

def increase_temperature(self, amount):

print(f"提高锅炉的温度{amount}度")

self.temperature += amount

def decrease_temperature(self, amount):

print(f"降低锅炉的温度{amount}度")

self.temperature -= amount


			The next step is to add the grammar, which we already covered. We will also create a `boiler` instance and print its default state:

word = Word(alphanums)

command = Group(OneOrMore(word))

token = Suppress("->")

device = Group(OneOrMore(word))

argument = Group(OneOrMore(word))

event = command + token + device + Optional(token + argument)

boiler = Boiler()


			The simplest way to retrieve the parsed output of pyparsing is by using the `parseString()` method. The result is a `ParseResults` instance, which is a parse tree that can be treated as a nested list. For example, executing `print(event.parseStri'g('increase -> boiler temperature -> 3 degr'es'))` would give‘`[['incre'se']' ['boi'er', 'temperat're']' ''3', 'degr'es']]` as a result.
			So, in this case, we know that the first sublist is the *command* (increase), the second sublist is the *receiver* (boiler temperature), and the third sublist is the *argument* (3°). We can unpack the `ParseResults` instance, which gives us direct access to these three parts of the event. Having direct access means that we can match patterns to find out which method should be executed:

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)


			Executing the preceding code snippet (using `python ch05/interpreter/boiler.py`) gives the following output:

将锅炉的温度提高 3 度

ch05/interpreter/interpreter.py 文件)与我所描述的并没有太大区别。它只是扩展以支持更多的事件和设备。以下是对步骤的总结:

            1.  首先,我们从`pyparsing`导入所有需要的。

            1.  我们定义以下类:`Gate`、`Aircondition`、`Heating`、`Boiler`(已介绍)和`Fridge`。

            1.  接下来,我们有我们的主函数:

1.  我们使用以下变量`tests`、`open_actions`和`close_actions`为将要执行的测试准备参数。

1.  我们执行测试动作。

        执行`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
tests tuple. However, the user wants to be able to activate events using an interactive prompt. Do not forget to check how sensitive pyparsing is regarding spaces, tabs, or unexpected input. For example, what happens if the user types turn off -> heating 37?
			The Strategy pattern
			Several solutions often exist for the same problem. Consider the task of sorting, which involves arranging the elements of a list in a particular sequence. For example, a variety of sorting algorithms are available for the task of sorting. Generally, no single algorithm outperforms all others in every situation.
			Selecting a sorting algorithm depends on various factors, tailored to the specifics of each case. Some key considerations include the following:

				*   **The number of elements to be sorted, known as the input size**: While most sorting algorithms perform adequately with a small input size, only a select few maintain efficiency with larger datasets.
				*   **The best/average/worst time complexity of the algorithm**: Time complexity is (roughly) the amount of time the algorithm takes to complete, excluding coefficients and lower-order terms. This is often the most usual criterion to pick an algorithm, although it is not always sufficient.
				*   **The space complexity of the algorithm**: Space complexity is (again roughly) the amount of physical memory needed to fully execute an algorithm. This is very important when we are working with big data or embedded systems, which usually have limited memory.
				*   **Stability of the algorithm**: An algorithm is considered stable when it maintains the relative order of elements with equal values after it is executed.
				*   **Code complexity of the algorithm**: If two algorithms have the same time/space complexity and are both stable, it is important to know which algorithm is easier to code and maintain.

			Other factors might also influence the choice of a sorting algorithm. The key consideration is whether a single algorithm must be applied universally. The answer is, unsurprisingly, no. It is more practical to have access to various sorting algorithms and choose the most suitable one for a given situation, based on the criteria. That’s what the Strategy pattern is about.
			The Strategy pattern promotes using multiple algorithms to solve a problem. Its killer feature is that it makes it possible to switch algorithms at runtime transparently (the client code is unaware of the change). So, if you have two algorithms and you know that one works better with small input sizes, while the other works better with large input sizes, you can use Strategy to decide which algorithm to use based on the input data at runtime.
			Real-world examples
			Reaching an airport to catch a flight is a good real-life Strategy example:

				*   If we want to save money and we leave early, we can go by bus/train
				*   If we don’t mind paying for a parking place and have our own car, we can go by car
				*   If we don’t have a car but we are in a hurry, we can take a taxi

			There are trade-offs between cost, time, convenience, and so forth.
			In software, Python’s `sorted()` and `list.sort()` functions are examples of the Strategy pattern. Both functions accept a named parameter key, which is basically the name of the function that implements a sorting strategy (*Python 3 Patterns, Recipes, and Idioms, by Bruce Eckel & Friends*, page 202).
			Use cases for the Strategy pattern
			Strategy is a very generic design pattern with many use cases. In general, whenever we want to be able to apply different algorithms dynamically and transparently, Strategy is the way to go. By different algorithms, I mean different implementations of the same algorithm. This means that the result should be the same, but each implementation has a different performance and code complexity (as an example, think of sequential search versus binary search).
			Apart from its usage for sorting algorithms as we mentioned, the Strategy pattern is used to create different formatting representations, either to achieve portability (for example, line-breaking differences between platforms) or dynamically change the representation of data.
			Implementing the Strategy pattern
			There is not much to be said about implementing the Strategy pattern. In languages where functions are not first-class citizens, each Strategy should be implemented in a different class. In Python, functions are objects (we can use variables to reference and manipulate them) and this simplifies the implementation of Strategy.
			Assume that we are asked to implement an algorithm to check whether all characters in a string are unique. For example, the algorithm should return true if we enter the dream string because none of the characters are repeated. If we enter the pizza string, it should return false because the letter “z” exists two times. Note that the repeated characters do not need to be consecutive, and the string does not need to be a valid word. The algorithm should also return false for the 1r2a3ae string because the letter “a” appears twice.
			After thinking about the problem carefully, we come up with an implementation that sorts the string and compares all characters pair by pair. First, we implement the `pairs()` function, which returns all neighbors pairs of a sequence, `seq`:

def pairs(seq):

n = len(seq)

for i in range(n):

yield seq[i], seq[(i + 1) % n]


			Next, we implement the `allUniqueSort()` function, which accepts a string, `s`, and returns `True` if all characters in the string are unique; otherwise, it returns `False`. To demonstrate the Strategy pattern, we will simplify by assuming that this algorithm fails to scale. We assume that it works fine for strings that are up to five characters. For longer strings, we simulate a slowdown by inserting a sleep statement:

SLOW = 3  # in seconds

LIMIT = 5  # in characters

WARNING"= "太糟糕了,你选择了慢算法"😦"

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


			We are not happy with the performance of `allUniqueSort()`, and we are trying to think of ways to improve it. After some time, we come up with a new algorithm, `allUniqueSet()`, that eliminates the need to sort. In this case, we use a set. If the character in check has already been inserted in the set, it means that not all characters in the string are unique:

def allUniqueSet(s):

if len(s) < LIMIT:

print(WARNING)

time.sleep(SLOW)

return True if len(set(s)) == len(s) else False


			Unfortunately, while `allUniqueSet()` has no scaling problems, for some strange reason, it performs worse than `allUniqueSort()` when checking short strings. What can we do in this case? Well, we can keep both algorithms and use the one that fits best, depending on the length of the string that we want to check.
			The `allUnique()` function accepts an input string, `s`, and a strategy function, `strategy`, which, in this case, is one of `allUniqueSort()` and `allUniqueSet()`. The `allUnique()` function executes the input strategy and returns its result to the caller.
			Then, the `main()` function lets the user perform the following actions:

				*   Enter the word to be checked for character uniqueness
				*   Choose the pattern that will be used

			It also does some basic error handling and gives the ability to the user to quit gracefully:

def main():

WORD_IN_DESC = "插入单词(输入 quit 退出)> "

STRAT_IN_DESC = "选择策略:[1] 使用集合,[2] 排序并配对> "

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"错误的选项:{strategy_picked}")


			Here’s a summary of the complete code for our implementation example (the `ch05/strategy.py` file):

				1.  We import the `time` module.
				2.  We define the `pairs()` function.
				3.  We define the values for the `SLOW`, `LIMIT`, and `WARNING` constants.
				4.  We define the function for the first algorithm, `allUniqueSort()`.
				5.  We define the function for the second algorithm, `allUniqueSet()`.
				6.  Next, we define the `allUnique()` function that helps call a chosen algorithm by passing the corresponding strategy function.
				7.  Finally, we add the `main()` function.

			Let’s see the output of a sample execution using the `python` `ch05/strategy.py` command:

插入单词(输入 quit 退出)> 气球

选择策略:[1] 使用集合,[2] 排序并配对> 1

allUnique(balloon): False

插入单词(输入 quit 退出)> 气球

选择策略:[1] 使用集合,[2] 排序并配对> 2

太糟糕了,你选择了慢速算法 😦

allUnique(balloon): False

插入单词(输入 quit 退出)> bye

选择策略:[1] 使用集合,[2] 排序并配对> 1

太糟糕了,你选择了慢速算法 😦

allUnique(bye): True

插入单词(输入 quit 退出)> bye

选择策略:[1] 使用集合,[2] 排序并配对> 2

allUnique(bye): True

balloon,有超过五个字符且不是所有字符都是唯一的。在这种情况下,两个算法都返回正确的结果,False,但 allUniqueSort()较慢,用户再次被警告。

        第二个单词,`bye`,少于五个字符且所有字符都是唯一的。再次,两个算法都返回预期的结果,`True`,但这次`allUniqueSet()`较慢,用户再次被警告。

        通常,我们想要使用的策略不应该由用户选择。策略模式的目的在于它使得使用不同的算法变得透明。修改代码,使得更快的算法总是被选中。

        我们代码的通常用户有两个。一个是终端用户,他们应该对代码中发生的事情一无所知,为了达到这个目的,我们可以遵循上一段中给出的建议。另一个可能的用户类别是其他开发者。假设我们想要创建一个将被其他开发者使用的 API。我们如何让他们对策略模式一无所知?一个建议是将两个函数封装在一个公共类中,例如,`AllUnique`。在这种情况下,其他开发者只需要创建该类的实例并执行一个方法,例如,`test()`。这个方法中需要做什么?

        备忘录模式

        在许多情况下,我们需要一种方法来轻松地捕捉一个对象的内部状态,以便在需要时能够恢复该对象。备忘录(Memento)是一种设计模式,可以帮助我们为这类情况实现解决方案。

        备忘录设计模式有三个关键组件:

            +   **备忘录**:一个包含基本状态存储和检索能力的简单对象

            +   **发起者**:一个可以获取和设置备忘录实例值的对象

            +   **保管者**:一个可以存储和检索所有先前创建的备忘录实例的对象

        备忘录模式与命令模式有很多相似之处。

        现实世界的例子

        备忘录模式在现实生活中的许多情况下都可以看到。

        例如,我们可以在一个语言(如英语或法语)的字典中找到这样的例子。该字典通过学术专家的工作定期更新,新词被添加,其他词变得过时。口语和书面语言都在不断发展,官方字典必须反映这一点。有时,我们会回顾以前的版本,以了解语言在某个过去时刻的使用情况。这也可以是因为信息在长时间后可能丢失,为了找到它,你可能需要查看旧版本。这有助于理解某个特定领域的知识。进行研究的某人可以使用旧字典或去档案馆查找有关某些单词和表达式的信息。

        此示例可以扩展到其他书面材料,如书籍和报纸。

        **Zope** ([`www.zope.org`](http://www.zope.org)),其集成的对象数据库称为**Zope 对象数据库**(**ZODB**),提供了一个很好的软件示例,展示了备忘录模式。它以其**通过 Web**的对象管理界面而闻名,为网站管理员提供了撤销支持。ZODB 是 Python 的对象数据库,在 Pyramid 和 Plone 等框架中被广泛使用。

        备忘录模式的用例

        备忘录通常用于为用户提供某种撤销和重做功能。

        另一种用法是实现带有 OK/Cancel 按钮的 UI 对话框,在加载对象时存储对象的状态,如果用户选择取消,我们将恢复对象的初始状态。

        实现备忘录模式

        我们将以简化的方式实现备忘录,并按照 Python 语言的自然方式来进行。这意味着我们不一定需要多个类。

        我们将使用 Python 的`pickle`模块。`pickle`模块有什么用?根据模块的文档([`docs.python.org/3/library/pickle.html`](https://docs.python.org/3/library/pickle.html)),`pickle`模块可以将复杂对象转换为字节流,也可以将字节流转换回具有相同内部结构的对象。

        警告

        在这里,我们使用`pickle`模块是为了演示,但你应该知道它对于通用用途来说并不安全。

        让我们以一个`Quote`类为例,它具有`text`和`author`属性。为了创建备忘录,我们将使用该类的一个方法,`save_state()`,正如其名称所暗示的,它将使用`pickle.dumps()`函数转储对象的状态。这创建了备忘录:
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()`函数:
    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}."
        然后,在主函数中,我们可以像往常一样处理事情并测试我们的实现:
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()
    # Now, we found the author's name
    q1.author = "Marcus Tullius Cicero"
    print(f"\nWe found the author, and did an updated:\n{q1}")
    # Restoring previous state (Undo)
    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()
    # changes to the text
    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}")
    # Restoring previous state (Undo)
    q2.restore_state(q2_mem2)
    print(f"\nWe restored the 2nd version, the correct one:\n{q2}")
        下面是示例中步骤的总结(`ch05/memento.py`文件):

            1.  我们导入`pickle`模块。

            1.  我们定义了`Quote`类。

            1.  最后,我们添加主函数来测试实现。

        让我们通过使用`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 **
Original version:
To be you in a world that is constantly
trying to make you be something else is
the greatest accomplishment.
- By Ralph Waldo Emerson.
We fixed the text:
To be yourself in a world that is constantly
trying to make you something else is the greatest
accomplishment.
- By Ralph Waldo Emerson.
We fixed the text again:
To be yourself when the world is constantly
trying to make you something else is the greatest
accomplishment.
- By Ralph Waldo Emerson.
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.
The output shows the program does what we expected: we can restore a previous state for each of our Quote objects.
        迭代器模式

        在编程中,我们经常使用序列或对象集合,尤其是在算法和编写操作数据的程序时。我们可以将自动化脚本、API、数据驱动应用程序和其他领域考虑在内。在本章中,我们将看到一个在必须处理对象集合时非常有用的模式:迭代器模式。

        注意,根据维基百科给出的定义

        *迭代器模式是一种设计模式,其中使用迭代器遍历容器并访问容器的元素。迭代器模式将算法与容器解耦;在某些情况下,算法必然是容器特定的,因此不能* *解耦* *。*

        迭代器模式在 Python 环境中被广泛使用。正如我们将看到的,这导致迭代器成为语言特性。它如此有用,以至于语言开发者决定将其作为一个特性。

        迭代器模式的用例

        当你想要以下行为之一或多个时,使用迭代器模式是个好主意:

            +   使导航集合变得容易

            +   在任何时刻获取集合中的下一个对象

            +   完成遍历集合后停止

        实现迭代器模式

        迭代器在 Python 中为我们实现,在 for 循环、列表推导式等中。Python 中的迭代器简单地说是一个可以迭代的对象;一个一次返回一个数据元素的对象。

        我们可以使用迭代器协议为自己的特殊情况进行实现,这意味着我们的迭代器对象必须实现两个特殊方法:`__iter__()`和`__next__()`。

        如果我们可以从一个对象中获得迭代器,则该对象被称为可迭代的。Python 中的大多数内置容器(列表、元组、集合、字符串等)都是可迭代的。`iter()`函数(它反过来调用`__iter__()`方法)从它们返回迭代器。

        让我们考虑一个我们想要使用`FootballTeam`类实现的足球队。如果我们想要将其转换为迭代器,我们必须实现迭代器协议,因为它不是一个内置容器类型,如列表类型。基本上,内置的`iter()`和`next()`函数不会对其起作用,除非它们被添加到实现中。

        首先,我们定义了迭代器的类,`FootballTeamIterator`,它将被用来遍历足球队伍对象。`members` 属性允许我们使用容器对象(它将是一个 `FootballTeam` 实例)初始化迭代器对象。我们向它添加了一个 `__iter__()` 方法,该方法将返回对象本身,以及一个 `__next__()` 方法,在每个调用中返回队伍中的下一个人,直到我们到达最后一个人。这将允许通过迭代器遍历足球队伍的成员。`FootballTeamIterator` 类的整个代码如下:
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)
        我们添加了一个小的主函数来测试我们的实现。一旦我们有了 `FootballTeam` 实例,我们就在它上面调用 `iter()` 函数来创建迭代器,然后使用 `while` 循环遍历它:
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` 文件):

            1.  我们定义了迭代器的类。

            1.  我们定义了容器类。

            1.  我们定义了主函数,然后是调用它的代码片段。

        这里是执行 `python ch05/iterator.py` 命令时得到的输出:
player1
player2
player3
player4
player5
...
player22
coach1
coach2
coach3
(End) string instead.
			The Template pattern
			A key ingredient in writing good code is avoiding redundancy. In OOP, methods and functions are important tools that we can use to avoid writing redundant code.
			Remember the `sorted()` example we saw when discussing the Strategy pattern. That function is generic enough that it can be used to sort more than one data structure (lists, tuples, and named tuples) using arbitrary keys. That’s the definition of a good function.
			Functions such as `sorted()` demonstrate the ideal case. However, we cannot always write 100% generic code.
			In the process of writing code that handles algorithms in the real world, we often end up writing redundant code. That’s the problem solved by the Template design pattern. This pattern focuses on eliminating code redundancy. The idea is that we should be able to redefine certain parts of an algorithm without changing its structure.
			Real-world examples
			The daily routine of a worker, especially for workers of the same company, is very close to the Template design pattern. All workers follow the same routine, but specific parts of the routine are very different.
			In software, Python uses the Template pattern in the `cmd` module, which is used to build line-oriented command interpreters. Specifically, `cmd.Cmd.cmdloop()` implements an algorithm that reads input commands continuously and dispatches them to action methods. What is done before the loop, after the loop, and at the command parsing part is always the same. This is also called the **invariant** part of an algorithm. The elements that change are the actual action methods (the variant part).
			Use cases for the Template pattern
			The Template design pattern focuses on eliminating code repetition. If we notice that there is repeatable code in algorithms that have structural similarities, we can keep the invariant (common) parts of the algorithms in a Template method/function and move the variant (different) parts in action/hook methods/functions.
			Pagination is a good use case to use Template. A pagination algorithm can be split into an abstract (invariant) part and a concrete (variant) part. The invariant part takes care of things such as the maximum number of lines/pages. The variant part contains functionality to show the header and footer of a specific page that is paginated.
			All application frameworks make use of some form of the Template pattern. When we use a framework to create a graphical application, we usually inherit from a class and implement our custom behavior. However, before this, a Template method is usually called, which implements the part of the application that is always the same, which is drawing the screen, handling the event loop, resizing and centralizing the window, and so on (*Python 3 Patterns, Recipes and Idioms, by Bruce Eckel & Friends*, page 133).
			Implementing the Template pattern
			In this example, we will implement a banner generator. The idea is rather simple. We want to send some text to a function, and the function should generate a banner containing the text. Banners have some sort of style, for example, dots or dashes surrounding the text. The banner generator has a default style, but we should be able to provide our own style.
			The `generate_banner()` function is our Template function. It accepts, as an input, the text (`msg`) that we want our banner to contain, and the style (`style`) that we want to use. The `generate_banner()` function wraps the styled text with a simple header and footer. The header and footer can be much more complex, but nothing forbids us from calling functions that can do the header and footer generations instead of just printing simple strings:

def generate_banner(msg, style):

print("-- 标签开始 --")

print(style(msg))

print("-- 标签结束 --\n")


			The `dots_style()` function simply capitalizes `msg` and prints 10 dots before and after it:

def dots_style(msg):

msg = msg.capitalize()

ten_dots = "." * 10

msg = f"{ten_dots}{msg}{ten_dots}"

return msg


			Another style that is supported by the generator is `admire_style()`. This style shows the text in uppercase and puts an exclamation mark between each character of the text:

def admire_style(msg):

msg = msg.upper()

return "!".join(msg)


			The next style is by far my favorite. The `cow_style()` style executes the `milk_random_cow()` method of `cowpy`, which is used to generate a random ASCII art character every time `cow_style()` is executed. Here is the `cow_style()` function:

def cow_style(msg):

msg = cow.milk_random_cow(msg)

return msg


			The `main()` function sends the `"happy coding"` text to the banner and prints it to the standard output using all the available styles:

def main():

styles = (dots_style, admire_style, cow_style)

msg = "happy coding"

[generate_banner(msg, style) for style in styles]


			Here is the recap of the full code of the example (the `ch05/template.py` file):

				1.  We import the `cow` function from `cowpy`.
				2.  We define the `generate_banner()` function.
				3.  We define the `dots_style()` function.
				4.  Next, we define the `admire_style()` and `cow_style()` functions.
				5.  We finish with the main function and the snippet to call it.

			Let’s look at a sample output by executing `python ch05/template.py` (note that your `cow_style()` output might be different due to the randomness of `cowpy`):
			![Figure 5.3 – Sample art output of the ch05/template.py program](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-py-dsnptn-2e/img/B21896_05_03.jpg)

			Figure 5.3 – Sample art output of the ch05/template.py program
			Do you like the art generated by `cowpy`? I certainly do. As an exercise, you can create your own style and add it to the banner generator.
			Another good exercise is to try implementing your own *Template* example. Find some existing redundant code that you wrote and see whether this pattern is applicable.
			Other behavioral design patterns
			What about the other behavioral design patterns from the Gang of Four’s catalog? We also have the **Mediator pattern** and the **Visitor pattern**:

				*   The Mediator pattern promotes loose coupling between objects by encapsulating how they interact and communicate with each other. In this pattern, objects don’t communicate directly with each other; instead, they communicate through a mediator object. This mediator object acts as a central hub that coordinates communication between the objects. The Mediator pattern stands out as a solution for promoting loose coupling and managing complex interactions between objects.
				*   For complex use cases, the Visitor pattern provides a solution for separating algorithms from the objects on which they operate. By allowing new operations to be defined without modifying the classes of the elements on which they operate, the Visitor pattern promotes flexibility and extensibility in object-oriented systems.

			We are not going to discuss these two patterns, since they are not commonly used by Python developers. Python offers built-in features and libraries that can help achieve loose coupling and/or extensibility goals without the need to implement these patterns. For example, one can use event-driven programming with a library such as `asyncio` instead of communication between objects through a mediator object. Additionally, using functions as first-class citizens, decorators, or context managers can provide ways to encapsulate algorithms and operations without the need for explicit visitor objects.
			Summary
			In this chapter, we discussed the behavioral design patterns.
			First, we covered the Chain of Responsibility pattern, which simplifies the management of complex processing flows, making it a valuable tool for enhancing flexibility and maintainability in software design.
			Second, we went over the Command pattern, which encapsulates a request as an object, thereby allowing us to parameterize clients with queues, requests, and operations. It also allows us to support undoable operations. Although the most advertised feature of command by far is undo, it has more uses. In general, any operation that can be executed at the user’s will at runtime is a good candidate for using the Command pattern.
			We looked at the Observer pattern, which helps with the separation of concerns, increasing decoupling between the publisher and subscribers. We have seen that observers are loosely coupled with the subject and can be added or removed dynamically.
			Then, we went over the State pattern, which is an implementation of one or more state machines used to solve a particular software engineering problem. A state machine can have only one active state at any point in time. A transition is a switch from the current state to a new state. It is normal to execute one or more actions before or after a transition occurs. State machines can be represented visually using state diagrams. State machines are used to solve many computational and non-computational problems. We saw how to implement a state machine for a computer system process using the `state_machine` module. The `state_machine` module simplifies the creation of a state machine and the definition of actions before/after transitions.
			Afterward, we looked at the Interpreter pattern, which is used to offer a programming-like framework to advanced users and domain experts, without exposing the complexities of a programming language. This is achieved by implementing a DSL, a computer language that has limited expressiveness and targets a specific domain. The interpreter is related to what are called internal DSLs. Although parsing is generally not addressed by the Interpreter pattern, as an implementation example, we used pyparsing to create a DSL that controls a smart house and saw that using a good parsing tool makes interpreting the results using pattern matching simple.
			Then, we looked at the Strategy design pattern, which is generally used when we want to be able to use multiple solutions for the same problem, transparently. There is no perfect algorithm for all input data and all cases, and by using Strategy, we can dynamically decide which algorithm to use in each case. We saw how Python, with its first-class functions, simplifies the implementation of Strategy by implementing two different algorithms that check whether all of the characters in a word are unique.
			Next, we looked at the Memento pattern, which is used to store the state of an object when needed. Memento provides an efficient solution when implementing some sort of undo capability for your users. Another usage is the implementation of a UI dialog with OK/Cancel buttons, where, if the user chooses to cancel, we will restore the initial state of the object. We used an example to get a feel for how Memento, in a simplified form and using the `pickle` module from the standard library, can be used in an implementation where we want to be able to restore previous states of data objects.
			We then looked at the Iterator pattern, which gives a nice and efficient way to iterate through sequences and collections of objects. In real life, whenever you have a collection of things and you are getting to those things one by one, you are using a form of the Iterator pattern. In Python, Iterator is a language feature. We can use it immediately on built-in containers such as lists and dictionaries, and we can define new iterable and iterator classes, to solve our problem, by using the Python iterator protocol. We saw that with an example of implementing a football team.
			Then, we saw how we can use the Template pattern to eliminate redundant code when implementing algorithms with structural similarities. We saw how the daily routine of a worker resembles the Template pattern. We also mentioned two examples of how Python uses Template in its libraries. General use cases of when to use Template were also mentioned. We concluded by implementing a banner generator, which uses a Template function to implement custom text styles.
			There are other structural design patterns: Mediator and Visitor. They are not commonly used by Python developers; therefore, we have not discussed them.
			In the next chapter, we will explore architectural design patterns, which are patterns that help in solving common architectural problems.



第三部分:超越四人帮

这一部分超出了经典设计模式,帮助你解决特殊软件设计需求,如微服务、基于云的应用程序和性能优化。它还讨论了测试模式和特定的 Python 反模式

  • 第六章, 架构设计模式

  • 第七章, 并发和异步模式

  • 第八章, 性能模式

  • 第九章, 分布式系统模式

  • 第十章, 测试模式

  • 第十一章, Python 反模式

第六章:架构设计模式

在上一章中,我们介绍了 行为模式,这些模式有助于对象交互和算法。下一个设计模式类别是 架构设计模式。这些模式提供了解决常见架构问题的模板,促进了可扩展、可维护和可重用系统的开发。

在本章中,我们将涵盖以下主要主题:

  • 模型-视图-控制器MVC) 模式

  • 微服务 模式

  • 无服务器 模式

  • 事件 溯源 模式

  • 其他架构设计模式

在本章结束时,你将了解如何使用流行的架构设计模式构建健壮且灵活的软件。

技术需求

请参阅 第一章 中提出的需求。本章讨论的代码的附加技术需求如下:

  • 对于 微服务模式 部分,请安装以下内容:

    • 使用命令:python -m pip install grpcio

    • python -m pip install grpcio-tools

    • python -m pip install "lanarky[openai]"==0.8.6 uvicorn==0.29.0 (注意,在撰写本文时,这可能与 Python 3.12 不兼容。在这种情况下,您可以使用 Python 3.11 来重现相关示例。)

  • 对于 无服务器模式 部分,请安装以下内容:

    • 使用命令:python –m pip install localstack (注意,在撰写本文时,这可能与 Python 3.12 不兼容。您可以使用 Python 3.11 来替代。)

    • 使用命令:python -m pip install awscli-local

    • 使用命令:python -m pip install awscli

  • 对于 事件溯源 部分,请安装以下内容:

    • 使用命令:python –m pip install eventsourcing

MVC 模式

MVC 模式是 松耦合 原则的另一种应用。该模式的名字来源于用于分割软件应用的三个主要组件:模型、视图和控制器。

即使我们永远不需要从头开始实现它,我们也需要熟悉它,因为所有常见的框架都使用 MVC 或其略有不同的版本(关于这一点稍后还会详细介绍)。

模型是核心组件。它代表知识。它包含并管理应用的(业务)逻辑、数据、状态和规则。视图是模型的视觉表示。视图的例子包括计算机 GUI、计算机终端的文本输出、智能手机的应用 GUI、PDF 文档、饼图、柱状图等等。视图只显示数据;它不处理数据。控制器是模型和视图之间的链接/粘合剂。模型和视图之间的所有通信都通过控制器进行。

使用 MVC 的应用程序的典型用法,在初始屏幕渲染给用户后,如下所示:

  1. 用户通过点击(输入、触摸等)按钮来触发视图。

  2. 视图会通知控制器用户的行为。

  3. 控制器处理用户输入并与模型交互。

  4. 该模型执行所有必要的验证和状态更改,并通知控制器应该做什么。

  5. 控制器根据模型的指示指导视图更新并适当地显示输出。

图 6.1 – MVC 模式

图 6.1 – MVC 模式

但是控制器部分是必要的吗?我们能不能跳过它?我们可以,但那样我们就会失去 MVC 提供的一个大优势:能够在不修改模型的情况下使用多个视图(甚至同时使用,如果我们想这样做)。为了在模型及其表示之间实现解耦,每个视图通常都需要自己的控制器。如果模型直接与特定的视图通信,我们就无法使用多个视图(或者至少不能以干净和模块化的方式使用)。

现实世界中的例子

MVC 是关注点分离原则的应用。在现实生活中,关注点分离被大量使用。例如,如果你建造一栋新房子,你通常会指派不同的专业人士来做以下工作:1)安装管道和电线;以及 2)粉刷房子。

另一个例子是餐厅。在餐厅里,服务员接收订单并为顾客上菜,但饭菜是由厨师烹制的。

在 Web 开发中,多个框架使用 MVC(模型-视图-控制器)理念,例如:

  • Web2py 框架是一个轻量级的 Python 框架,它采用了 MVC 模式。项目网站上有很多示例演示了如何在 Web2py 中使用 MVC([web2py.com/](http://web2py.com/))以及在 GitHub 仓库中。

  • Django([https://www.djangoproject.com/](https://www.djangoproject.com/))也是一个 MVC 框架,尽管它使用了不同的命名约定。控制器被称为视图,视图被称为模板。Django 使用模型-视图-模板(MVT)的名称。根据 Django 的设计师,视图描述了用户看到的数据,因此它使用视图作为特定 URL 的 Python 回调函数。在 Django 中,“模板”一词用于将内容与表示分离。它描述了用户看到数据的方式,而不是看到哪些数据。

MVC 模式的使用案例

MVC 是一个非常通用且有用的设计模式。实际上,所有流行的 Web 框架(Django、Rails、Symfony 和 Yii)以及应用程序框架(iPhone SDK、Android 和 QT)都使用了 MVC 或其变体(例如模型-视图-适配器(MVA)、模型-视图-表示器(MVP)或MVT)。然而,即使我们不使用这些框架,由于它提供的优势,自己实现这个模式也是有意义的,这些优势如下:

  • 视图和模型之间的分离使得图形设计师可以专注于用户界面(UI)部分,程序员可以专注于开发,而不会相互干扰。

  • 由于视图和模型之间的松散耦合,每个部分都可以在不影响其他部分的情况下进行修改/扩展。例如,添加一个新的视图是微不足道的。只需为它实现一个新的控制器即可。

  • 维护每个部分更容易,因为责任是清晰的。

当从头开始实现 MVC 时,确保你创建智能模型、瘦控制器和愚视图。

一个模型被认为是智能的,因为它执行以下操作:

  • 它包含所有验证/业务规则/逻辑

  • 它处理应用程序的状态

  • 它可以访问应用程序数据(数据库、云等)

  • 它不依赖于 UI

一个控制器被认为是瘦的,因为它执行以下操作:

  • 当用户与视图交互时,它更新模型

  • 当模型更改时,它更新视图

  • 如果需要,在将数据交付给模型/视图之前处理数据

  • 它不显示数据

  • 它不直接访问应用程序数据

  • 它不包含验证/业务规则/逻辑

一个视图被认为是愚的,因为它执行以下操作:

  • 它显示数据

  • 它允许用户与之交互

  • 它只进行最小处理,通常由模板语言提供(例如,使用简单的变量和循环控制)

  • 它不存储任何数据

  • 它不直接访问应用程序数据

  • 它不包含验证/业务规则/逻辑

如果你从头开始实现 MVC 并且想知道你是否做得正确,你可以尝试回答一些关键问题:

  • 如果你的应用程序有一个 GUI,它是可定制的吗?你有多容易改变它的皮肤/外观和感觉?你能否在运行时给用户改变应用程序皮肤的能力?如果这并不简单,这意味着你的 MVC 实现出了问题。

  • 如果你的应用程序没有 GUI(例如,如果它是一个终端应用程序),添加 GUI 支持有多难?或者,如果添加 GUI 是不相关的,添加视图以在图表(饼图、条形图等)或文档(PDF、电子表格等)中显示结果是否容易?如果这些更改不是微不足道的(只需创建一个新的控制器并将其附加到视图上,而不修改模型),则 MVC 没有正确实现。

  • 如果你确保这些条件得到满足,与不使用 MVC 的应用程序相比,你的应用程序将更加灵活和易于维护。

实现 MVC 模式

我可以使用任何常见的框架来演示如何使用 MVC,但我感觉这样会不完整。所以,我决定向你展示如何从头开始实现 MVC,使用一个非常简单的例子:一个引言打印机。这个想法极其简单。用户输入一个数字,看到与该数字相关的引言。引言存储在quotes元组中。这是通常存在于数据库、文件等中的数据,并且只有模型可以直接访问它。

让我们考虑这个quotes元组的例子:

quotes = (
    "A man is not complete until he is married. Then he is finished.",
    "As I said before, I never repeat myself.",
    "Behind a successful man is an exhausted woman.",
    "Black holes really suck...",
    "Facts are stubborn things.",
)

模型是最简化的;它只有一个get_quote()方法,该方法根据索引nquotes元组返回引语(字符串)。模型类如下所示:

class QuoteModel:
    def get_quote(self, n):
        try:
            value = quotes[n]
        except IndexError as err:
            value = "Not found!"
        return value

视图有三个方法:show(),用于在屏幕上打印引语(或Not found!消息);error(),用于在屏幕上打印错误消息;以及select_quote(),用于读取用户的选项。这可以在下面的代码中看到:

class QuoteTerminalView:
    def show(self, quote):
        print(f'And the quote is: "{quote}"')
    def error(self, msg):
        print(f"Error: {msg}")
    def select_quote(self):
        return input("Which quote number would you like to see? ")

控制器负责协调。__init__()方法初始化模型和视图。run()方法验证用户给出的引用索引,从模型中获取引语,并将其传递回视图以显示,如下面的代码所示:

class QuoteTerminalController:
    def __init__(self):
        self.model = QuoteModel()
        self.view = QuoteTerminalView()
    def run(self):
        valid_input = False
        while not valid_input:
            try:
                n = self.view.select_quote()
                n = int(n)
                valid_input = True
            except ValueError as err:
                self.view.error(f"Incorrect index '{n}'")
        quote = self.model.get_quote(n)
        self.view.show(quote)

最后,main()函数初始化并启动控制器,如下面的代码所示:

def main():
    controller = QuoteTerminalController()
    while True:
        controller.run()

下面是我们例子的总结(完整代码在ch06/mvc.py文件中):

  1. 我们首先定义了一个用于引语列表的变量。

  2. 我们定义了模型类,QuoteModel

  3. 我们定义了视图类,QuoteTerminalView

  4. 我们定义了控制器类,QuoteTerminalController

  5. 最后,我们添加了main()函数来测试不同的类,然后是通常的技巧来调用它。

执行python ch06/mvc.py命令的示例显示了程序如何将引语打印给用户:

Which quote number would you like to see? 3
And the quote is: "Black holes really suck..."
Which quote number would you like to see? 2
And the quote is: "Behind a successful man is an exhausted woman."
Which quote number would you like to see? 6
And the quote is: "Not found!"
Which quote number would you like to see? 4
And the quote is: "Facts are stubborn things."
Which quote number would you like to see? 3
And the quote is: "Black holes really suck..."
Which quote number would you like to see? 1
And the quote is: "As I said before, I never repeat myself."

微服务模式

传统上,从事构建服务器端应用程序的开发人员一直使用单个代码库,并在那里实现所有或大多数功能,使用常见的开发实践,如函数和类,以及我们在本书中迄今为止涵盖的设计模式。

然而,随着 IT 行业的演变、经济因素以及快速上市和投资回报的压力,我们需要不断改进工程团队的做法,并确保服务器、服务交付和运营具有更高的反应性和可扩展性。我们需要了解其他有用的模式,而不仅仅是面向对象的编程模式。

图 6.2 – 微服务模式

图 6.2 – 微服务模式

近年来,工程师模式目录的主要新增内容是微服务架构模式或微服务。其理念是我们可以将应用程序构建为一组松散耦合、协作的服务。在这种架构风格中,一个应用程序可能包括订单管理服务、客户管理服务等这样的服务。这些服务是松散耦合的、独立部署的,并通过定义良好的 API 进行通信。

现实世界中的例子

我们可以引用几个例子,如下所示:

  • Netflix:采用微服务处理数百万内容流的先驱之一

  • Uber:该公司使用微服务来处理不同的方面,如计费、通知和行程跟踪

  • 亚马逊:他们从单体架构过渡到微服务架构,以支持其不断增长的规模

微服务模式的用例

我们可以想到几个用例,在这些用例中,微服务提供了巧妙的解决方案。每次我们构建一个具有以下至少一个特征的应用程序时,我们都可以使用基于微服务架构的设计:

  • 需要支持不同的客户端,包括桌面和移动设备

  • 存在一个 API 供第三方消费

  • 我们必须使用消息传递与其他应用程序进行通信

  • 我们通过访问数据库、与其他系统通信并返回正确的响应类型(JSON、XML、HTML 或甚至是 PDF)来处理请求

  • 应用程序的不同功能区域对应着逻辑组件

实现微服务模式——使用 gRPC 的支付服务

让我们简要地谈谈在微服务世界中软件安装和应用部署。从部署单个应用程序转变为部署许多小型服务意味着需要处理的事物数量呈指数级增长。虽然你可能对单个应用程序服务器和一些运行时依赖项感到满意,但当迁移到微服务时,依赖项的数量将急剧增加。例如,一个服务可能受益于关系型数据库,而另一个则可能需要ElasticSearch。你可能需要一个使用MySQL的服务,另一个则可能使用Redis服务器。因此,采用微服务方法也意味着你需要使用容器

多亏了 Docker,事情变得更容易,因为我们可以将这些服务作为容器运行。想法是,你的应用程序服务器、依赖项和运行时库、编译后的代码、配置等等,都包含在这些容器中。然后,你所要做的就是运行打包成容器的服务,并确保它们可以相互通信。

你可以直接使用 Django、Flask 或 FastAPI 来实现微服务模式,用于 Web 应用程序或 API。然而,为了快速展示一个工作示例,我们将使用 gRPC,这是一个高性能的通用 RPC 框架,它使用Protocol Buffersprotobuf)作为其接口描述语言,由于其效率和跨语言支持,使其成为微服务通信的理想选择。

想象一个场景,其中你的应用程序架构包括一个专门处理支付处理的微服务。这个微服务(让我们称它为PaymentService),负责处理支付并与OrderServiceAccountService等其他服务交互。我们将专注于使用 gRPC 实现此类服务的实现。

首先,我们在ch06/microservices/grpc/payment.proto文件中定义服务和其方法,使用 protobuf。这包括指定请求和响应消息格式:

syntax = "proto3";
package payment;
// The payment service definition.
service PaymentService {
  // Processes a payment
  rpc ProcessPayment (PaymentRequest) returns (PaymentResponse) {}
}
// The request message containing payment details.
message PaymentRequest {
  string order_id = 1;
  double amount = 2;
  string currency = 3;
  string user_id = 4;
}
// The response message containing the result of the payment process.
message PaymentResponse {
  string payment_id = 1;
  string status = 2; // e.g., "SUCCESS", "FAILED"
}

然后,您必须使用 protobuf 编译器(protoc)将payment.proto文件编译成 Python 代码。为此,您需要使用特定的命令行,该命令行调用protoc并带有适用于 Python 的适当插件和选项。

这里是用于在 Python 中使用 gRPC 编译.proto文件的命令行的一般形式:

cd ch06/microservices/grpc), and then we run the following command:

payment_pb2.py 和 payment_pb2_grpc.py。这些文件不应手动编辑。

        接下来,我们在`payment_service.py`文件中提供支付处理的服务逻辑,扩展了在生成的`.py`文件中提供的内容。在该模块中,我们定义了`PaymentServiceImpl`类,继承自`payment_pb2_grpc.PaymentServiceServicer`类,并重写了`ProcessPayment()`方法,该方法将执行处理支付所需的工作(例如,调用外部 API,执行数据库更新等)。请注意,这里是一个简化的示例,但您会有更复杂的逻辑。代码如下:
from concurrent.futures import ThreadPoolExecutor
import grpc
import payment_pb2
import payment_pb2_grpc
class PaymentServiceImpl(payment_pb2_grpc.PaymentServiceServicer):
    def ProcessPayment(self, request, context):
        return payment_pb2.PaymentResponse(payment_id="12345", status="SUCCESS")
        然后,我们有`main()`函数,其中包含启动处理服务的代码,通过调用`grpc.server(ThreadPoolExecutor(max_workers=10))`创建。函数的代码如下:
def main():
    print("Payment Processing Service ready!")
    server = grpc.server(ThreadPoolExecutor(max_workers=10))
    payment_pb2_grpc.add_PaymentServiceServicer_to_server(PaymentServiceImpl(), server)
    server.add_insecure_port("[::]:50051")
    server.start()
    server.wait_for_termination()
        这样,服务就完成了,并准备好进行测试。我们需要一个客户端来测试它。我们可以编写一个测试客户端,使用 gRPC 调用服务的代码(在`ch06/microservices/grpc/client.py`文件中):
import grpc
import payment_pb2
import payment_pb2_grpc
with grpc.insecure_channel("localhost:50051") as chan:
    stub = payment_pb2_grpc.PaymentServiceStub(chan)
    resp = stub.ProcessPayment(
        payment_pb2.PaymentRequest(
            order_id="order123",
            amount=99.99,
            currency="USD",
            user_id="user456",
        )
    )
    print("Payment Service responded.")
    print(f"Response status: {resp.status}")
        要启动服务(在`ch06/microservices/grpc/payment_service.py`文件中),您可以运行以下命令:
python ch06/microservices/grpc/payment_service.py
        您将得到以下输出,显示服务已按预期启动:
ch06/microservices/grpc/client.py file):

python ch06/microservices/grpc/client.py


			In the terminal where you have run the client code, you should get the following output:

支付服务响应。

响应状态:成功


			This output is what is expected.
			Note that while gRPC is a powerful choice for Microservices communication, other approaches such as **REST over HTTP** can also be used, especially when human readability or web integration is a priority. However, gRPC provides advantages in terms of performance and support for streaming requests and responses, and it was interesting to introduce it with this example.
			Implementing the microservices pattern – an LLM service using Lanarky
			Lanarky is a web framework that builds upon the FastAPI framework, to provide batteries for building Microservices that use **large language** **models** (**LLMs**).
			We will follow the *Getting started* instructions from the website ([`lanarky.ajndkr.com`](https://lanarky.ajndkr.com)) to showcase a microservice backed by Lanarky. To be able to test the example, you need to set the `OPENAI_API_KEY` environment variable to use OpenAI. Visit [`openai.com`](https://openai.com) and follow the instructions to get your API key.
			The LLM service code starts by importing the modules we need:

导入 os

导入 uvicorn

从 lanarky 导入 Lanarky

从 lanarky.adapters.openai.resources 导入 ChatCompletionResource

从 lanarky.adapters.openai.routing 导入 OpenAIAPIRouter


			Before starting the actual application code, you need to pass the OpenAI API key, which is used by Lanarky’s code via the `os.environ` object. For example, pass the value of the secret key via this line:

os.environ["OPENAI_API_KEY"] = "在此处输入您的 OpenAI API 密钥"


			Security practice
			It is recommended that you pass secret keys to the code, by setting an environment variable in your shell.
			Then, we create an `app` object, an instance of the `Lanarky` class, and the `router` object that will be used for the definition of the service’s routes, as is conventional with FastAPI. This router is an instance of the `OpenAPIRouter` class provided by the Lanarky framework:

app = Lanarky()

router = OpenAIAPIRouter()


			Next, we provide a `chat()` function for the `/chat` route, when there is a `POST` request, as follows:

@router.post("/chat")

def chat(stream: bool = True) -> ChatCompletionResource:

system = "这里是您的助手"

return ChatCompletionResource(stream=stream, system=system)


			Finally, we associate the router to the FastAPI application (standard FastAPI convention) and we run the FastAPI application (our service) using `uvicorn.run()`, as follows:

if name == "main":

app.include_router(router)

uvicorn.run(app)


			To finalize this demonstration implementation, we can write client code to interact with the service. The code for that part is as follows:

导入 click

导入 sys 模块

从 lanarky.clients 导入 StreamingClient

args = sys.argv[1:]

if len(args) == 1:

message = args[0]

client = StreamingClient()

for event in client.stream_response(

"POST",

"/chat",

params={"stream": "false"},

json={"messages": [dict(role="user", content=message)]},

):

print(f"{event.event}: {event.data}")

else:

print("您需要传递一条消息!")


			To test the example, similarly to the previous one (where we tested a gRPC-based microservice), open a terminal, and run the LLM service code (in the `ch06/microservices/lanarky/llm_service.py` file) using the following command:

python ch06/microservices/lanarky/llm_service.py


			You should get an output like the following:

INFO:启动服务器进程[18617]

INFO:等待应用程序启动。

INFO:应用程序启动完成。

INFO:Uvicorn 正在运行于 http://127.0.0.1:8000(按 CTRL+C 退出)


			Then, open a second terminal to run the client program, using the following command:

python ch06/microservices/lanarky/client.py "Hello"


			You should get the following output:

completion: Hello! How can I assist you today?


			Now, you can continue sending messages via the client program, and wait for the service to come back with the completion, as you would do via the ChatGPT interface.
			For example, see the following code:

python ch06/microservices/lanarky/client.py "瑞士的首都是什么?"

完成:瑞士的首都是伯尔尼。


			The Serverless pattern
			The Serverless pattern abstracts server management, allowing developers to focus solely on code. Cloud providers handle the scaling and execution based on event triggers, such as HTTP requests, file uploads, or database modifications.
			![Figure 6.3 – The Serverless pattern](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-py-dsnptn-2e/img/B21896_06_03.jpg)

			Figure 6.3 – The Serverless pattern
			The Serverless pattern is particularly useful for Microservices, APIs, and event-driven architectures.
			Real-world examples
			There are several examples we can think of for the Serverless pattern. Here are some of them:

				*   **Automated data backups**: Serverless functions can be scheduled to automatically back up important data to cloud storage
				*   **Image processing**: Whenever a user uploads an image, a serverless function can automatically resize, compress, or apply filters to the image
				*   **PDF generation for E-commerce receipts**: After a purchase is made, a serverless function generates a PDF receipt and emails it to the customer

			Use cases for the Serverless pattern
			There are two types of use cases the Serverless pattern can be used for.
			First, Serverless is useful for handling event-driven architectures where specific functions need to be executed in response to events, such as doing image processing (cropping, resizing) or dynamic PDF generation.
			The second type of architecture where Serverless can be used is **Microservices**. Each microservice can be a serverless function, making it easier to manage and scale.
			Since we have already discussed the Microservices pattern in the previous section, we are going to focus on how to implement the first use case.
			Implementing the Serverless pattern
			Let’s see a simple example using AWS Lambda to create a function that squares a number. AWS Lambda is Amazon’s serverless **compute** service, which runs code in response to triggers such as changes in data, shifts in system state, or actions by users.
			There is no need to add more complexity since there’s already enough to get right with the Serverless architecture itself and AWS Lambda’s deployment details.
			First, we need to write the Python code for the function. We create a `lambda_handler()` function, which takes two parameters, `event` and `context`. In our case, the input number is accessed as a value of the “number” key in the event dictionary. We take the square of that value and we return a a string containing the expected result. The code is as follows:

import json

def lambda_handler(event, context):

number = event["number"]

squared = number * number

return f"{number} 的平方是 {squared}。"


			Once we have the Python function, we need to deploy it so that it can be invoked as an AWS Lambda function. For our learning, instead of going through the procedure of deploying to AWS infrastructure, we can use a method that consists of testing things locally. This is what the `LocalStack` Python package allows us to do. Once it is installed, from your environment, you can start LocalStack inside a Docker container by running the available executable in your Python environment, using the command:

ch06/lambda_function_square.py) 打包成一个 ZIP 文件,例如,可以使用 ZIP 程序如下:

awslocal tool (a Python module we need to install). Once installed, we can use this program to deploy the Lambda function into the “local stack” AWS infrastructure. This is done, in our case, using the following command:

awslocal lambda create-function \

--function-name lambda_function_square \

--runtime python3.11 \

--zip-file fileb://lambda.zip \

--handler lambda_function_square.lambda_handler \

--role arn:aws:iam::000000000000:role/lambda-role


			Adapt to your Python version
			At the time of writing, this was tested with Python 3.11\. You must adapt this command to your Python version.
			You can test the Lambda function, providing an input using the `payload.json` file, using the command:

awslocal lambda invoke --function-name lambda_function_square \

output.txt 文件的内容。你应该看到以下文本:

awslocal, running the following command:

awslocal lambda create-function-url-config \

--function-name lambda_function_square \

http://.lambda-url.us-east-1.localhost.localstack.cloud:4566 格式。

        现在,例如,我们可以使用 `cUrl` 触发 Lambda 函数 URL:
curl -X POST \
    'http://iu4s187onr1oabg50dbvm77bk6r5sunk.lambda-url.us-east-1.localhost.localstack.cloud:4566/' \
    -H 'Content-Type: application/json' \
    -d '{"number": 6}'
        有关 AWS Lambda 的最新和详细指南,请参阅[`docs.aws.amazon.com/lambda/`](https://docs.aws.amazon.com/lambda/)文档。

        这是一个最小示例。另一个无服务器应用程序的例子可能是一个为业务生成 PDF 发票的功能。这将使业务无需担心服务器管理,只需为消耗的计算时间付费。

        事件溯源模式

        事件溯源模式将状态更改存储为一系列事件,允许重建过去的状态并提供审计跟踪。这种模式在状态复杂且转换的业务规则复杂的系统中特别有用。

        正如我们将在后面的实现示例中看到的那样,事件溯源模式强调捕获应用程序状态的所有更改作为一系列事件的必要性。其结果之一是,应用程序状态可以通过重新播放这些事件在任何时间点重建。

        现实世界例子

        在软件类别中存在几个现实世界的例子:

            +   **审计跟踪**:为了符合规定,记录对数据库所做的所有更改

            +   **协作编辑**:允许多个用户同时编辑一个文档

            +   **撤销/重做功能**:提供在应用程序中撤销或重做操作的能力

        事件溯源模式的使用案例

        事件溯源模式有几个使用案例。让我们考虑以下三个:

            +   **金融交易**:事件溯源可用于记录账户余额的每次更改作为一个不可变事件的时序序列。这种方法确保每次存款、取款或转账都被捕获为一个独立的事件。这样,我们可以提供一个透明、可审计和安全的财务活动账本。

            +   **库存管理**:在库存管理背景下,事件源通过记录所有更改作为事件来帮助跟踪每个项目的生命周期。这使得企业能够保持库存水平的准确和最新记录,识别项目使用或销售中的模式,并预测未来的库存需求。它还便于追溯任何项目的历史,有助于召回过程或质量保证调查。

            +   **客户行为跟踪**:事件源在捕获和存储客户与平台互动的每一个交互中扮演着关键角色,从浏览历史和购物车修改到购买和退货。这些丰富的数据,以一系列事件的形式结构化,成为分析客户行为、个性化营销策略、提升用户体验和改进产品推荐的有价值资源。

        现在,让我们看看我们如何实现这个模式。

        实现事件源模式 - 手动方式

        让我们从一些定义开始。事件源模式实现的组成部分如下:

            +   **事件**:状态变化的表示,通常包含事件类型和与该事件相关联的数据。一旦创建并应用了事件,就不能再更改。

            +   **聚合**:表示单个业务逻辑或数据单元的对象(或对象组)。它跟踪事物,每次有事物发生变化(一个事件)时,它都会记录下来。

            +   **事件存储**:所有已发生事件的集合。

        通过通过事件处理状态变化,业务逻辑变得更加灵活且易于扩展。例如,添加新类型的事件或修改现有事件的处理方式可以以最小的系统影响完成。

        在这个第一个例子中,对于银行账户用例,我们将看到如何以手动方式实现事件源模式。在这种实现中,你通常会定义你的事件类并手动编写将这些事件应用到聚合上的逻辑。让我们看看。

        我们首先定义一个`Account`类,它代表一个具有余额和附加到账户操作的事件列表的银行账户。这个类作为聚合。它的`events`属性代表事件存储。在这里,一个事件将由一个包含操作类型(“存入”或“取出”)和金额值的字典表示。

        然后我们添加了一个接受事件作为输入的`apply_event()`方法。根据`event["type"]`,我们根据事件的数量增加或减少账户余额,并将事件添加到`events`列表中,从而有效地存储事件:
class Account:
    def __init__(self):
        self.balance = 0
        self.events = []
    def apply_event(self, event):
        if event["type"] == "deposited":
            self.balance += event["amount"]
        elif event["type"] == "withdrawn":
            self.balance -= event["amount"]
        self.events.append(event)
        然后,我们添加一个`deposit()`方法和一个`withdraw()`方法,这两个方法都调用`apply_event()`方法,如下所示:
    def deposit(self, amount):
        event = {"type": "deposited", "amount": amount}
        self.apply_event(event)
    def withdraw(self, amount):
        event = {"type": "withdrawn", "amount": amount}
        self.apply_event(event)
        最后,我们添加了`main()`函数,如下所示:
def main():
    account = Account()
    account.deposit(100)
    account.deposit(50)
    account.withdraw(30)
    account.deposit(30)
    for evt in account.events:
        print(evt)
    print(f"Balance: {account.balance}")
        运行代码,使用`python ch06/ event_sourcing/bankaccount.py`命令,得到以下输出:
{'type': 'deposited', 'amount': 100}
{'type': 'deposited', 'amount': 50}
{'type': 'withdrawn', 'amount': 30}
{'type': 'deposited', 'amount': 30}
Balance: 150
        本例通过一个简单的手动实现,为我们提供了对事件源(Event Sourcing)的第一理解。对于更复杂的系统,专门为事件源设计的框架和库可以帮助管理一些复杂性,提供事件存储、查询和处理的实用工具。我们将接下来测试这样一个库。

        使用库实现事件源模式

        在这个第二个例子中,我们将使用`eventsourcing`库来实现事件源模式。让我们考虑一个库存管理系统,其中我们跟踪物品的数量。

        我们首先导入所需的模块,如下所示:
from eventsourcing.domain import Aggregate, event
from eventsourcing.application import Application
        然后,我们通过继承`Aggregate`类来定义聚合对象的类`InventoryItem`。该类有一个`increase_quantity()`和`decrease_quantity`方法,每个方法都装饰了`@event`装饰器。这个类的代码如下所示:
class InventoryItem(Aggregate):
    @event("ItemCreated")
    def __init__(self, name, quantity=0):
        self.name = name
        self.quantity = quantity
    @event("QuantityIncreased")
    def increase_quantity(self, amount):
        self.quantity += amount
    @event("QuantityDecreased")
    def decrease_quantity(self, amount):
        self.quantity -= amount
        接下来,我们创建我们的库存应用程序类`InventoryApp`,它继承自`eventsourcing`库的`Application`类。第一个方法处理创建一个项目,接受`InventoryItem`类的实例(`item`),并使用该项目在`InventoryApp`对象上调用`save()`方法。但`save()`方法究竟做了什么?它从给定的聚合中收集挂起的事件,并将它们放入应用的事件存储中。类的定义如下所示:
class InventoryApp(Application):
    def create_item(self, name, quantity):
        item = InventoryItem(name, quantity)
        self.save(item)
        return item.id
        接下来,类似于我们在上一个例子中所做的,我们添加了一个`increase_item_quantity()`方法,它处理项目数量的增加(对于聚合对象),然后在该应用上保存聚合对象,随后是相应的`decrease_item_quantity()`方法,用于减少操作,如下所示:
    def increase_item_quantity(self, item_id, amount):
        item = self.repository.get(item_id)
        item.increase_quantity(amount)
        self.save(item)
    def decrease_item_quantity(self, item_id, amount):
        item = self.repository.get(item_id)
        item.decrease_quantity(amount)
        self.save(item)
        最后,我们添加了`main()`函数,其中包含一些测试我们设计的代码,如下所示:
def main():
    app = InventoryApp()
    # Create a new item
    item_id = app.create_item("Laptop", 10)
    # Increase quantity
    app.increase_item_quantity(item_id, 5)
    # Decrease quantity
    app.decrease_item_quantity(item_id, 3)
    notifs = app.notification_log.select(start=1, limit=5)
    notifs = [notif.state for notif in notifs]
    for notif in notifs:
        print(notif.decode())
        运行代码,使用`python ch06/event_sourcing/inventory.py`命令,得到以下输出:
{"timestamp":{"_type_":"datetime_iso","_data_":"2024-03-18T08:05:10.583875+00:00"},"originator_topic":"__main__:InventoryItem","name":"Laptop","quantity":10}
{"timestamp":{"_type_":"datetime_iso","_data_":"2024-03-18T08:05:10.584818+00:00"},"amount":5}
eventsourcing library, which makes it easier to implement this type of application.
			Other architectural design patterns
			You may encounter documentation about other architectural design patterns. Here are three other patterns:

				*   **Event-Driven Architecture (EDA)**: This pattern emphasizes the production, detection, consumption of, and reaction to events. EDA is highly adaptable and scalable, making it suitable for environments where systems need to react to significant events in real time.
				*   **Command Query Responsibility Segregation (CQRS)**: This pattern separates the models for reading and writing data, allowing for more scalable and maintainable architectures, especially when there are clear distinctions between operations that mutate data and those that only read data.
				*   **Clean Architecture**: This pattern proposes a way to organize code such that it encapsulates the business logic but keeps it separate from the interfaces through which the application is exposed to users or other systems. It emphasizes the use of dependency inversion to drive the decoupling of software components.

			Summary
			In this chapter, we explored several foundational architectural design patterns that are pivotal in modern software development, each useful for different requirements and solving unique challenges.
			We first covered the MVC pattern, which promotes the separation of concerns by dividing the application into three interconnected components. This separation allows for more manageable, scalable, and testable code by isolating the UI, the data, and the logic that connects the two.
			Then, we looked at the Microservices pattern, which takes a different approach by structuring an application as a collection of small, independent services, each responsible for a specific business function. This pattern enhances scalability, flexibility, and ease of deployment, making it an ideal choice for complex, evolving applications that need to rapidly adapt to changing business requirements.
			Next, we looked at the Serverless pattern, which shifts the focus from server management to pure business logic by leveraging cloud services to execute code snippets in response to events. This pattern offers significant cost savings, scalability, and productivity benefits by abstracting the underlying infrastructure, allowing developers to concentrate on writing code that adds direct value.
			Afterward, we went over the Event Sourcing pattern, which offers another way to handle data changes in an application by storing each change as a sequence of events. This not only provides a robust audit trail and enables complex business functionalities but also allows the system to reconstruct past states, offering invaluable insights into the data life cycle and changes over time.
			Lastly, we touched upon other architectural design patterns, such as CQRS and Clean Architecture. Each offers unique advantages and addresses different aspects of software design and architecture. Even if we could not dive deep into these patterns, they complement the developer’s toolkit for building well-structured and maintainable systems.
			In the next chapter, we will discuss concurrency and asynchronous patterns and techniques to help our program manage multiple operations simultaneously or move on to other tasks while waiting for operations to complete.





第七章:并发和异步模式

在上一章中,我们介绍了架构设计模式:这些模式有助于解决复杂项目带来的某些独特挑战。接下来,我们需要讨论并发和异步模式,这是我们的解决方案目录中的另一个重要类别。

并发允许你的程序同时管理多个操作,充分利用现代处理器的全部能力。这就像一位厨师并行准备多道菜,每个步骤都精心编排,以确保所有菜肴同时准备好。另一方面,异步编程允许你的应用程序在等待操作完成时继续执行其他任务,例如将食物订单发送到厨房,并在订单准备好之前为其他顾客提供服务。

在本章中,我们将涵盖以下主要内容:

  • 线程池模式

  • 工作模型模式

  • 未来与承诺模式

  • 响应式编程中的观察者模式

  • 其他并发和异步模式

技术要求

请参阅第一章中提出的各项要求。本章讨论的代码的附加技术要求如下:

  • Faker,使用 pip install faker

  • ReactiveX,使用 pip install reactivex

线程池模式

首先,了解什么是线程很重要。在计算机中,线程是操作系统可以调度的最小的处理单元。

线程就像可以在计算机上同时运行的执行轨迹,这使得许多活动可以同时进行,从而提高性能。它们在需要多任务处理的应用程序中尤为重要,例如处理多个 Web 请求或执行多个计算。

现在,让我们转向线程池模式本身。想象一下,你有很多任务要完成,但启动每个任务(在这种情况下,创建一个线程)在资源和时间上可能很昂贵。这就像每次有工作要做时都雇佣一个新员工,工作完成后又让他们离开。这个过程可能效率低下且成本高昂。通过维护一个或多个可以一次性创建并多次重用的工作线程集合,线程池模式有助于降低这种低效。当一个线程完成一个任务后,它不会终止,而是回到线程池中,等待另一个可以再次使用的任务。

什么是工作线程?

工作线程是特定任务或任务集的执行线程。工作线程用于将处理任务从主线程卸载,通过异步执行耗时或资源密集型任务来帮助保持应用程序的响应性。

除了更快的应用程序性能外,还有两个好处:

  • 降低开销:通过重用线程,应用程序避免了为每个任务创建和销毁线程的开销

  • 更好的资源管理:线程池限制了线程的数量,防止了由于创建过多线程而可能发生的资源耗尽

现实世界中的例子

在现实生活中,想象一家小餐馆,餐馆里有有限数量的厨师(线程)为顾客烹饪餐点(任务)。由于厨房空间(系统资源)的限制,餐馆一次只能容纳一定数量的厨师同时工作。当新的订单到来时,如果所有厨师都在忙碌,订单就会在队列中等待,直到有可用的厨师。这样,餐馆通过其可用的厨师有效地管理订单流,确保所有厨师都得到有效利用,而不会压垮厨房或需要为每个新订单雇佣更多员工。

在软件中也有很多例子:

  • 网络服务器经常使用线程池来处理传入的客户请求。这允许它们同时为多个客户提供服务,而不需要为每个请求创建新的线程。

  • 数据库使用线程池来管理连接,确保总有一池连接可供传入的查询使用。

  • 任务调度器使用线程池来执行计划的任务,例如cron作业、备份或更新。

线程池模式的用例

有三个用例,其中线程池模式有助于:

  • 批量处理:当你有许多可以并行执行的任务时,线程池可以将它们分配给其工作线程

  • 负载均衡:线程池可以用来在工作线程之间均匀地分配工作负载,确保没有单个线程承担过多的工作

  • 资源优化:通过重用线程,线程池最小化了系统资源的使用,例如内存和 CPU 时间

实现线程池模式

首先,让我们停下来分析一下对于给定的应用程序,线程池是如何工作的:

  1. 当应用程序启动时,线程池创建一定数量的工作线程。这是初始化。线程的数量可以是固定的,也可以根据应用程序的需求动态调整。

  2. 然后,我们有任务提交步骤。当有任务要执行时,它被提交到池中,而不是直接创建一个新的线程。任务可以是任何需要执行的内容,例如处理用户输入、处理网络请求或执行计算。

  3. 下一步是任务执行。池将任务分配给一个可用的工作线程。如果所有线程都在忙碌,任务可能会在队列中等待,直到有线程变得可用。

  4. 一旦线程完成了其任务,它不会死亡。相反,它返回到池中,准备好被分配新的任务。

对于我们的示例,让我们看看一些代码,其中我们创建了一个包含五个工作线程的线程池来处理一组任务。我们将使用concurrent.futures模块中的ThreadPoolExecutor类。

我们首先导入示例中需要的模块,如下所示:

from concurrent.futures import ThreadPoolExecutor
import time

然后,我们创建一个函数来模拟任务,在这个例子中,我们简单地使用 time.sleep(1)

def task(n):
    print(f"Executing task {n}")
    time.sleep(1)
    print(f"Task {n} completed")

然后,我们使用一个 ThreadPoolExecutor 类的实例,该实例创建了一个最大工作者线程数为 5 的线程池,并向线程池提交了 10 个任务。因此,工作者线程会取走这些任务并执行它们。一旦工作者线程完成一个任务,它会从队列中取走另一个。代码如下:

with ThreadPoolExecutor(max_workers=5) as executor:
    for i in range(10):
        executor.submit(task, i)

当运行示例代码时,使用 ch07/thread_pool.py Python 命令,你应该得到以下输出:

Executing task 0
Executing task 1
Executing task 2
Executing task 3
Executing task 4
Task 0 completed
Task 4 completed
Task 3 completed
Task 1 completed
Executing task 6
Executing task 7
Executing task 8
Task 2 completed
Executing task 5
Executing task 9
Task 8 completed
Task 6 completed
Task 9 completed
Task 5 completed
Task 7 completed

我们看到,任务的完成顺序与提交顺序不同。这表明它们是使用线程池中可用的线程并发执行的。

Worker Model 模式

Worker Model 模式的背后思想是将一个大型任务或多个任务划分为更小、更易于管理的单元,称为工作者,这些单元可以并行处理。这种并发和并行处理方法不仅加速了处理时间,还提高了应用程序的性能。

工作者可以是单个应用程序内的线程(正如我们在线程池模式中刚刚看到的),同一台机器上的独立进程,甚至是分布式系统中的不同机器。

Worker Model 模式的优点如下:

  • 可扩展性:易于通过添加更多工作者进行扩展,这在分布式系统中特别有益,因为可以在多台机器上处理任务

  • 效率:通过将任务分配给多个工作者,系统可以更好地利用可用的计算资源,并行处理任务

  • 灵活性:Worker Model 模式可以适应各种处理策略,从简单的基于线程的工作者到跨越多服务器的复杂分布式系统

现实世界示例

考虑一个快递服务,包裹(任务)由一组快递员(工作者)递送。每个快递员从配送中心(任务队列)取走一个包裹并递送。快递员的数量可以根据需求变化;在繁忙时期可以增加更多快递员,而在较安静时可以减少。

在大数据处理中,Worker Model 模式通常被采用,其中每个工作者负责映射或减少数据的一部分。

在 RabbitMQ 或 Kafka 等系统中,Worker Model 模式用于并发处理队列中的消息。

我们还可以引用图像处理服务。需要同时处理多个图像的服务通常使用 Worker Model 模式在多个工作者之间分配负载。

Worker Model 模式的用例

Worker Model 模式的用例之一是数据转换。当你有一个需要转换的大型数据集时,你可以将工作分配给多个工作者。

另一个用例是任务并行化。在任务彼此独立的应用程序中,Worker Model 模式可以非常有效。

第三个用例是 分布式计算,工作模型模式可以扩展到多台机器,使其适用于分布式计算环境。

实现工作模型模式

在讨论实现示例之前,让我们了解工作模型模式是如何工作的。工作模型模式涉及三个组件:工作者、任务队列和可选的调度器:

  • 工作者:在这个模型中的主要角色。每个工作者可以独立于其他工作者执行任务的一部分。根据实现方式,工作者可能一次处理一个任务或同时处理多个任务。

  • 任务队列:一个中央组件,其中存储着等待处理的任务。工作者通常从这个队列中拉取任务,确保任务在他们之间高效分配。队列充当了一个缓冲区,将任务提交与任务处理解耦。

  • 调度器:在某些实现中,调度器组件根据可用性、负载或优先级将任务分配给工作进程。这有助于优化任务分配和资源利用。

现在我们来看一个并行执行函数的例子。

我们首先导入示例中需要的模块,如下所示:

from multiprocessing import Process, Queue
import time

然后,我们创建一个 worker() 函数,我们将用它来运行任务。该函数接受一个参数 task_queue 对象,其中包含要执行的任务。代码如下:

def worker(task_queue):
    while not task_queue.empty():
        task = task_queue.get()
        print(f"Worker {task} is processing")
        time.sleep(1)
        print(f"Worker {task} completed")

main() 函数中,我们首先创建一个任务队列,一个 multiprocessing.Queue 实例。然后,我们创建 10 个任务并将它们添加到队列中:

def main():
    task_queue = Queue()
    for i in range(10):
        task_queue.put(i)

接着,我们创建了五个工作进程,使用 multiprocessing.Process 类,并启动它们。每个工作进程从队列中取一个任务来执行,然后取另一个任务,直到队列为空。然后,我们通过循环启动每个工作进程(使用 p.start()),这意味着相关的任务将并发执行。之后,我们创建另一个循环,在这个循环中使用进程的 .join() 方法,这样程序会等待这些进程完成工作。这部分代码如下:

    processes = [
        Process(target=worker, args=(task_queue,))
        for _ in range(5)
    ]
    # Start the worker processes
    for p in processes:
        p.start()
    # Wait for all worker processes to finish
    for p in processes:
        p.join()
    print("All tasks completed.")

当运行示例代码时,使用 ch07/worker_model.py Python 命令,你应该得到以下输出,其中可以看到 5 个工作者以并发方式从任务队列中处理任务,直到所有 10 个任务完成:

Worker 0 is processing
Worker 1 is processing
Worker 2 is processing
Worker 3 is processing
Worker 4 is processing
Worker 0 completed
Worker 5 is processing
Worker 1 completed
Worker 6 is processing
Worker 2 completed
Worker 7 is processing
Worker 3 completed
Worker 8 is processing
Worker 4 completed
Worker 9 is processing
Worker 5 completed
Worker 6 completed
Worker 7 completed
Worker 8 completed
Worker 9 completed
All tasks completed.

这展示了我们实现的工作模型模式。这种模式特别适用于任务独立且可以并行处理的情况。

未来和承诺模式

在异步编程范式下,Future 表示一个尚未知晓但最终会提供的值。当一个函数启动异步操作时,它不会阻塞直到操作完成并得到结果,而是立即返回一个 Future。这个 Future 对象充当了稍后可用的实际结果的占位符。

未来对象通常用于 I/O 操作、网络请求和其他耗时的异步任务。它们允许程序在等待操作完成的同时继续执行其他任务。这种特性被称为非阻塞

一旦未来被实现,结果可以通过未来对象访问,通常是通过回调、轮询或阻塞,直到结果可用。

承诺是未来对象的可写、可控对应物。它代表异步操作的生产者端,最终将为相关的未来对象提供结果。当操作完成时,承诺通过一个值或错误被履行或拒绝,然后解决未来对象。

承诺可以被链式调用,允许一系列异步操作以清晰和简洁的方式执行。

通过允许程序在等待异步操作的同时继续执行,应用程序变得更加响应。另一个好处是可组合性:多个异步操作可以以干净和可管理的方式组合、排序或并行执行。

实际例子

从木匠那里订购定制餐桌提供了未来和承诺模式的实际例子。当你下单时,你会收到一个预计完成日期和设计草图(未来对象),代表木匠交付桌子的承诺。随着木匠的工作进行,这个承诺逐渐得到履行。完成餐桌的交付解决了未来对象,标志着木匠对你承诺的履行。

我们也可以在数字领域找到几个例子,如下所示:

  • 在线购物订单跟踪:当你在线下单时,网站会立即为你提供订单确认和跟踪号码(未来对象)。随着你的订单被处理、发货和交付,状态更新(承诺的履行)会在跟踪页面上实时反映,最终确定最终的交付状态。

  • 食品配送应用:通过食品配送应用下单后,你会收到一个预计的配送时间(未来对象)。应用会持续更新订单状态——从准备到取货和配送(承诺正在履行)——直到食物送到你家门口,此时未来对象因订单完成而得到解决。

  • 客户支持工单:当你在一个网站上提交支持工单时,你会立即收到一个工单号码和一条消息,说明有人会回复你(未来对象)。幕后,支持团队根据优先级或接收顺序处理工单。一旦你的工单得到处理,你会收到回复,履行了你最初提交工单时做出的承诺。

未来和承诺模式的用例

至少有四种情况下推荐使用未来和承诺模式:

  1. 数据处理管道:在数据处理管道中,数据通常需要经过多个阶段才能达到最终形式。通过用 Future 表示每个阶段,你可以有效地管理数据的异步流动。例如,一个阶段的输出可以作为下一个阶段的输入,但由于每个阶段都返回一个 Future,后续阶段不需要阻塞等待前一个阶段完成。

  2. 任务调度:任务调度系统,如操作系统或高级应用程序中的系统,可以使用 Future 来表示计划在未来运行的任务。当任务被调度时,会返回一个 Future 来表示该任务的最终完成。这允许系统或应用程序跟踪任务的状态,而不会阻塞执行。

  3. 复杂数据库查询或事务:异步执行数据库查询对于保持应用程序的响应性至关重要,尤其是在用户体验至关重要的 Web 应用程序中。通过使用 Future 来表示数据库操作的结果,应用程序可以发起一个查询并立即将控制权返回给用户界面或调用函数。Future 最终会解析为查询结果,允许应用程序更新 UI 或处理数据,而无需在等待数据库响应时冻结或变得无响应。

  4. 文件输入输出操作:文件输入输出操作可能会显著影响应用程序的性能,尤其是在主线程上同步执行时。通过应用 Future 和 Promise 模式,文件输入输出操作被卸载到后台进程,并返回一个 Future 来表示操作的完成。这种方法允许应用程序在读取或写入文件的同时继续运行其他任务或响应用户交互。一旦 I/O 操作完成,Future 就会解析,应用程序可以处理或显示文件数据。

在这些用例中,Future 和 Promise 模式促进了异步操作,允许应用程序通过不阻塞主线程执行长时间运行的任务,保持响应性和高效性。

使用concurrent.futures实现 Future 和 Promise 模式

要了解如何实现 Future 和 Promise 模式,你必须首先理解其机制的三个步骤。接下来,让我们逐一分析:

  1. 初始化:初始化步骤涉及使用一个函数启动异步操作,在该函数中,不是等待操作完成,而是函数立即返回一个“Future”对象。此对象充当稍后可用的结果的占位符。内部,异步函数创建一个“Promise”对象。此对象负责处理异步操作的结果。Promise 与 Future 相关联,这意味着 Promise 的状态(无论是已履行还是被拒绝)将直接影响 Future。

  2. 执行:在执行步骤中,操作独立于主程序流程进行。这允许程序保持响应性并继续执行其他任务。一旦异步任务完成,其结果需要传达给启动操作的部分程序。操作的结果(无论是成功的结果还是错误)传递给先前创建的 Promise。

  3. 解析:如果操作成功,Promise 将“履行”结果。如果操作失败,Promise 将“拒绝”错误。Promise 的履行或拒绝解决 Future。通常通过回调或后续函数使用结果,这是一段指定如何处理结果的代码。Future 提供机制(例如,方法或运算符)来指定这些回调,这些回调将在 Future 解决后执行。

在我们的示例中,我们使用ThreadPoolExecutor类的实例异步执行任务。submit 方法返回一个将最终包含计算结果的Future对象。我们首先导入所需的模块,如下所示:

from concurrent.futures import ThreadPoolExecutor, as_completed

然后,我们定义一个用于执行的任务的函数:

def square(x):
    return x * x

我们提交任务并获取Future对象,然后我们收集完成的 Future 对象。as_completed函数允许我们遍历完成的Future对象并检索它们的结果:

with ThreadPoolExecutor() as executor:
    future1 = executor.submit(square, 2)
    future2 = executor.submit(square, 3)
    future3 = executor.submit(square, 4)
    futures = [future1, future2, future3]
    for future in as_completed(futures):
        print(f"Result: {future.result()}")

运行示例时,使用ch07/future_and_promise/future.py Python 命令,你应该得到以下输出:

Result: 16
Result: 4
Result: 9

这展示了我们的实现。

实现 Future 和 Promise 模式 - 使用 asyncio

Python 的asyncio库提供了另一种使用异步编程执行任务的方法。它特别适用于 I/O 密集型任务。让我们看看使用这种技术的第二个示例。

什么是 asyncio?

asyncio库提供了对异步 I/O、事件循环、协程和其他并发相关任务的支撑。因此,使用asyncio,开发者可以编写高效处理 I/O 密集型操作的代码。

协程和 async/await

协程是一种特殊的函数,可以在某些点暂停和恢复其执行,同时允许其他协程在此期间运行。协程使用async关键字声明。此外,协程可以使用await关键字从其他协程中等待。

我们导入asyncio模块,它包含我们所需的一切:

import asyncio

然后,我们创建一个用于计算并返回数字平方的函数。我们还想进行 I/O 密集型操作,因此我们使用asyncio.sleep()。请注意,在asyncio风格的编程中,这样的函数使用组合关键字async def定义——它是一个协程。asyncio.sleep()函数本身也是一个协程,因此我们确保在调用它时使用await关键字:

async def square(x):
    # Simulate some IO-bound operation
    await asyncio.sleep(1)
    return x * x

然后,我们转向创建我们的main()函数。我们使用asyncio.ensure_future()函数来创建我们想要的Future对象,传递square(x),其中x是要平方的数字。我们创建了三个Future对象,future1future2future3。然后,我们使用asyncio.gather()协程等待我们的 Future 完成并收集结果。main()函数的代码如下:

async def main():
    fut1 = asyncio.ensure_future(square(2))
    fut2 = asyncio.ensure_future(square(3))
    fut3 = asyncio.ensure_future(square(4))
    results = await asyncio.gather(fut1, fut2, fut3)
    for result in results:
        print(f"Result: {result}")

在我们的代码文件末尾,我们有常见的if __name__ == "__main__":块。由于我们正在编写基于asyncio的代码,所以这里的新颖之处在于我们需要通过调用asyncio.run(main())来运行asyncio的事件循环:

if __name__ == "__main__":
    asyncio.run(main())

要测试示例,运行ch07/future_and_promise/async.py Python 命令。你应该得到以下类似的输出:

Result: 4
Result: 9
Result: 16

结果的顺序可能会根据运行程序的人和时间而变化。实际上,这是不可预测的。你可能已经注意到了我们之前示例中的类似行为。这通常是并发或异步代码的一般情况。

这个简单的例子表明,当我们需要高效处理 I/O 密集型任务(如网络爬取或 API 调用)时,asyncio是 Future 和 Promise 模式的合适选择。

响应式编程中的观察者模式

观察者模式(在第五章行为设计模式中介绍)在通知一个对象或一组对象给定对象的状态发生变化时非常有用。这种传统的观察者模式允许我们响应某些对象变化事件。它为许多情况提供了一个很好的解决方案,但在我们必须处理许多事件,其中一些相互依赖的情况下,传统的方法可能会导致复杂、难以维护的代码。这就是另一个称为响应式编程的范式给我们提供了一个有趣的选择的地方。简单来说,响应式编程的概念是在保持我们的代码干净的同时,对许多事件(事件流)做出反应。

让我们关注 ReactiveX (reactivex.io),它是响应式编程的一部分。ReactiveX 的核心是一个称为可观察的概念。根据其官方网站,ReactiveX 是关于提供异步编程 API,这些 API 被称为可观察流。这个概念被添加到我们已讨论的观察者理念中。

想象一个 Observable 就像一条河流,它将数据或事件流向一个 Observer。这个 Observable 依次发送项目。这些项目通过由不同步骤或操作组成的路径旅行,直到它们到达一个 Observer,该 Observer 接受或消费它们。

现实世界的例子

机场的航班信息显示系统在响应式编程中类似于一个 Observable。这样的系统会持续流式传输有关航班状态的更新,包括到达、出发、延误和取消。这个类比说明了观察者(旅客、航空公司员工和机场服务人员订阅以接收更新)如何订阅一个 Observable(航班显示系统)并对连续的更新流做出反应,从而允许对实时信息做出动态响应。

电子表格应用程序也可以被视为响应式编程的一个例子,基于其内部行为。在几乎所有电子表格应用程序中,交互式地更改工作表中的任何单元格都会导致立即重新评估直接或间接依赖于该单元格的所有公式,并更新显示以反映这些重新评估。

ReactiveX 思想在多种语言中得到实现,包括 Java(RxJava)、Python(RxPY)和 JavaScript(RxJS)。Angular 框架使用 RxJS 来实现 Observable 模式。

在响应式编程中使用观察者模式的用例

一个用例是集合管道的概念,由马丁·福勒在他的博客中讨论(martinfowler.com/articles/collection-pipeline)。

集合管道,由马丁·福勒描述

集合管道是一种编程模式,其中你将一些计算组织为一系列操作,这些操作通过将一个集合作为一个操作的输出并传递给下一个操作来组合。

在处理数据时,我们还可以使用 Observable 对对象序列执行“映射和归约”或“按组”等操作。

最后,可以创建用于各种函数的 Observables,例如按钮事件、请求和 Twitter 动态。

在响应式编程中实现观察者模式

对于这个例子,我们决定构建一个包含(虚构)人名的列表的流(在ch07/observer_rx/people.txt文本文件中),以及基于它的 Observable。

注意

提供了一个包含虚构人名的文本文件作为本书示例文件的一部分(ch07/observer_rx/people.txt)。但每当需要时,可以使用辅助脚本(ch07/observer_rx/peoplelist.py)生成一个新的文件,这个脚本将在下一分钟介绍。

这样的名字列表示例看起来可能如下所示:

Peter Brown, Gabriel Hunt, Gary Martinez, Heather Fernandez, Juan White, Alan George, Travis Davidson, David Adams, Christopher Morris, Brittany Thomas, Brian Allen, Stefanie Lutz, Craig West, William Phillips, Kirsten Michael, Daniel Brennan, Derrick West, Amy Vazquez, Carol Howard, Taylor Abbott,

回到我们的实现。我们首先导入所需的模块:

from pathlib import Path
import reactivex as rx
from reactivex import operators as ops

我们定义了一个函数firstnames_from_db(),它从一个包含名字的文本文件(读取文件内容)返回一个 Observable,使用flat_map()filter()map()方法进行转换,并使用一个新的操作group_by()来从另一个序列中发射项目——文件中找到的第一个名字及其出现次数:

def firstnames_from_db(path: Path):
    file = path.open()
    # collect and push stored people firstnames
    return rx.from_iterable(file).pipe(
        ops.flat_map(
            lambda content: rx.from_iterable(
                content.split(", ")
            )
        ),
        ops.filter(lambda name: name != ""),
        ops.map(lambda name: name.split()[0]),
        ops.group_by(lambda firstname: firstname),
        ops.flat_map(
            lambda grp: grp.pipe(
                ops.count(),
                ops.map(lambda ct: (grp.key, ct)),
            )
        ),
    )

然后,在main()函数中,我们定义了一个每 5 秒发射数据的 Observable,将其发射与从firstnames_from_db(db_file)返回的内容合并,在将db_file设置为包含人名的文本文件之后,如下所示:

def main():
    db_path = Path(__file__).parent / Path("people.txt")
    # Emit data every 5 seconds
    rx.interval(5.0).pipe(
        ops.flat_map(lambda i: firstnames_from_db(db_path))
    ).subscribe(lambda val: print(str(val)))
    # Keep alive until user presses any key
    input("Starting... Press any key and ENTER, to quit\n")

这里是对示例的总结(完整的代码在ch07/observer_rx/rx_peoplelist.py文件中):

  1. 我们导入所需的模块和类。

  2. 我们定义了一个firstnames_from_db()函数,它从一个文本文件返回一个 Observable,该文件是数据的来源。我们从该文件收集并推送存储的人名。

  3. 最后,在main()函数中,我们定义了一个每 5 秒发射数据的 Observable,将其发射与调用firstnames_from_db()函数返回的内容合并。

要测试示例,请运行ch07/observer_rx/rx_peoplelist.py Python 命令。你应该得到以下输出(这里只显示了一部分):

Starting... Press any key and ENTER, to quit
('Peter', 1)
('Gabriel', 1)
('Gary', 1)
('Heather', 1)
('Juan', 1)
('Alan', 1)
('Travis', 1)
('David', 1)
('Christopher', 1)
('Brittany', 1)
('Brian', 1)
('Stefanie', 1)
('Craig', 1)
('William', 1)
('Kirsten', 1)
('Daniel', 1)
('Derrick', 1)

一旦你按下一个键并在键盘上按下Enter,发射就会中断,程序停止。

处理新的数据流

我们的测试是成功的,但从某种意义上说,它是静态的;数据流仅限于当前文本文件中的内容。我们现在需要生成多个数据流。我们可以使用一种基于第三方模块 Faker(pypi.org/project/Faker)的技术来生成文本文件中的假数据。生成数据的代码免费提供给你(在ch07/observer_rx/peoplelist.py文件中),如下所示:

from faker import Faker
import sys
fake = Faker()
args = sys.argv[1:]
if len(args) == 1:
    output_filename = args[0]
    persons = []
    for _ in range(0, 20):
        p = {"firstname": fake.first_name(), "lastname": fake.last_name()}
        persons.append(p)
    persons = iter(persons)
    data = [f"{p['firstname']} {p['lastname']}" for p in persons]
    data = ", ".join(data) + ", "
    with open(output_filename, "a") as f:
        f.write(data)
else:
    print("You need to pass the output filepath!")

现在,让我们看看执行这两个程序(ch07/observer_rx/peoplelist.pych07/observer_rx/rx_peoplelis.py)会发生什么:

  • 从一个命令行窗口或终端,你可以通过传递正确的文件路径到脚本中生成人名;你会执行以下命令:python ch07/observer_rx/peoplelist.py ch07/observer_rx/people.txt

  • 从第二个 shell 窗口,你可以通过执行python ch07/observer_rx/rx_peoplelist.py命令来运行实现 Observable 的程序。

那么,这两个命令的输出是什么?

创建了一个新的people.txt文件版本(其中包含用逗号分隔的随机名字),以替换现有文件。每次你重新运行该命令(python ch07/observer_rx/peoplelist.py),都会向文件中添加一组新的名字。

第二个命令给出的输出类似于第一次执行时的输出;区别在于现在发射的不是相同的数据集。现在,可以在源中生成新数据并发射。

其他并发和异步模式

开发者可能会使用一些其他的并发和异步模式。我们可以引用以下模式:

  • 演员模型:一个处理并发计算的概念模型。它定义了一些规则,说明演员实例应该如何行为:一个演员可以做出局部决策,创建更多演员,发送更多消息,并确定如何对收到的下一个消息做出反应。

  • asyncio库)。

  • 消息传递:用于并行计算、面向对象编程OOP)和进程间通信IPC),其中软件实体通过相互传递消息来通信和协调它们的行为。

  • 背压:一种管理通过软件系统中的数据流并防止组件过载的机制。它允许系统通过向生产者发出信号以减慢速度,直到消费者能够赶上,从而优雅地处理过载。

每个模式都有其适用场景和权衡。知道它们存在很有趣,但我们无法讨论所有可用的模式和技巧。

摘要

在本章中,我们讨论了并发和异步模式,这些模式对于编写高效、响应性软件,能够同时处理多个任务非常有用。

线程池模式是并发编程中的一个强大工具,它提供了一种有效管理资源并提高应用程序性能的方法。它帮助我们提高应用程序性能,同时减少开销并更好地管理资源,因为线程池限制了线程的数量。

虽然线程池模式侧重于重用固定数量的线程来执行任务,但工作模型模式更多地关注于在可能可扩展和灵活的工作实体之间动态分配任务。这种模式特别适用于任务独立且可以并行处理的情况。

未来和承诺模式促进了异步操作,通过不阻塞主线程执行长时间运行的任务,使应用程序保持响应性和高效。

我们还讨论了反应式编程中的观察者模式。这个模式的核心思想是对数据流和事件做出反应,就像我们在自然界中看到的水流一样。在计算世界中,我们有大量的这种想法的例子。我们讨论了一个 ReactiveX 的例子,这为读者提供了一个如何接近这种编程范式并继续通过 ReactiveX 官方文档进行自己研究的介绍。

最后,我们提到了还有其他并发和异步模式。每个模式都有其适用场景和权衡,但我们无法在一本书中涵盖所有这些模式。

在下一章中,我们将讨论性能设计模式。

第八章:性能模式

在上一章中,我们介绍了并发和异步模式,这些模式对于编写能够同时处理多个任务的效率软件非常有用。接下来,我们将讨论一些特定的性能模式,这些模式有助于提高应用程序的速度和资源利用率。

性能模式解决常见的瓶颈和优化挑战,为开发者提供经过验证的方法来提高执行时间、减少内存使用并有效扩展。

在本章中,我们将涵盖以下主要主题:

  • 缓存旁路模式

  • 缓存模式

  • 懒加载模式

技术要求

请参阅第一章中提出的要求。本章讨论的代码的附加技术要求如下:

  • 使用以下命令将Faker模块添加到您的 Python 环境中:python -m pip install faker

  • 使用以下命令将Redis模块添加到您的 Python 环境中:python -m pip install redis

  • 使用 Docker 安装 Redis 服务器并运行它:docker run --name myredis -p 6379:6379 redis

    如果需要,请遵循redis.io/docs/latest/上的文档。

缓存旁路模式

在数据读取频率高于更新的情况下,应用程序使用缓存来优化对存储在数据库或数据存储中的信息的重复访问。在某些系统中,这种类型的缓存机制是内置的,并且可以自动工作。当这种情况不成立时,我们必须在应用程序中自行实现它,使用适合特定用例的缓存策略。

其中一种策略被称为缓存旁路,在这种策略中,为了提高性能,我们将频繁访问的数据存储在缓存中,从而减少从数据存储中重复获取数据的需求。

现实世界案例

我们可以在软件领域引用以下示例:

  • Memcached 通常用作缓存服务器。它是一个流行的内存键值存储,用于存储来自数据库调用、API 调用或 HTML 页面内容的小块数据。

  • Redis 是另一种用于缓存的服务器解决方案。如今,它是我用于缓存或应用内存存储用例的首选服务器,在这些用例中,它表现出色。

  • 根据文档网站(docs.aws.amazon.com/elasticache/)的说明,亚马逊的 ElastiCache 是一种云服务,它使得在云中设置、管理和扩展分布式内存数据存储或缓存环境变得容易。

缓存旁路模式的使用案例

当我们需要在我们的应用程序中减少数据库负载时,缓存旁路模式非常有用。通过缓存频繁访问的数据,可以减少发送到数据库的查询次数。它还有助于提高应用程序的响应速度,因为缓存数据可以更快地检索。

注意,这种模式适用于不经常变化的数据,以及不依赖于存储中一组条目一致性的数据存储(多个键)。例如,它可能适用于某些类型的文档存储或数据库,其中键永远不会更新,偶尔会删除数据条目,但没有强烈的要求在一段时间内继续提供服务(直到缓存刷新)。

实现缓存旁路模式。

我们可以总结实现 Cache-Aside 模式所需的步骤,涉及数据库和缓存,如下所示:

  • 案例 1 – 当我们想要获取数据项时:如果缓存中找到该项,则从缓存中返回该项。如果没有在缓存中找到,则从数据库中读取数据。将我们得到的项目放入缓存并返回。

  • 案例 2 – 当我们想要更新数据项时:在数据库中写入该项,并从缓存中删除相应的条目。

让我们尝试一个简单的实现,使用一个数据库,用户可以通过应用程序请求检索一些引语。我们在这里的重点是实现案例 1部分。

这里是我们为这个实现需要在机器上安装的额外软件依赖项的选择:

  • SQLite 数据库,因为我们可以使用 Python 的标准模块 sqlite3 来查询 SQLite 数据库。

  • Redis 服务器和 redis-py Python 模块。

我们将使用一个脚本(在 ch08/cache_aside/populate_db.py 文件中)来处理创建数据库和 quotes 表,并将示例数据添加到其中。出于实际考虑,我们也在那里使用 Faker 模块生成假引语,这些引语用于填充数据库。

我们的代码从所需的导入开始,然后创建我们将用于生成假引语的 Faker 实例,以及一些常量或模块级变量:

import sqlite3
from pathlib import Path
from random import randint
import redis
from faker import Faker
fake = Faker()
DB_PATH = Path(__file__).parent / Path("quotes.sqlite3")
cache = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)

然后,我们编写一个函数来处理数据库设置部分,如下所示:

def setup_db():
    try:
        with sqlite3.connect(DB_PATH) as db:
            cursor = db.cursor()
            cursor.execute(
                """
                CREATE TABLE quotes(id INTEGER PRIMARY KEY, text TEXT)
            """
            )
            db.commit()
            print("Table 'quotes' created")
    except Exception as e:
        print(e)

然后,我们定义一个中心函数,该函数负责根据句子列表或文本片段添加一组新的引语。在众多事情中,我们将引语标识符与引语关联,用于数据库表中的 id 列。为了简化问题,我们只是随机选择一个数字,使用 quote_id = randint(1, 100)add_quotes() 函数定义如下:

def add_quotes(quotes_list):
    added = []
    try:
        with sqlite3.connect(DB_PATH) as db:
            cursor = db.cursor()
            for quote_text in quotes_list:
                quote_id = randint(1, 100) # nosec
                quote = (quote_id, quote_text)
                cursor.execute(
                    """INSERT OR IGNORE INTO quotes(id, text) VALUES(?, ?)""", quote
                )
                added.append(quote)
            db.commit()
    except Exception as e:
        print(e)
    return added

接下来,我们添加一个 main() 函数,实际上它将包含几个部分;我们想要使用命令行参数解析。请注意以下内容:

  • 如果我们传递 init 参数,我们调用 setup_db() 函数。

  • 如果我们传递 update_all 参数,我们将引语注入数据库并添加到缓存中。

  • 如果我们传递 update_db_only 参数,我们只将引语注入数据库。

当运行 Python 脚本时调用的 main() 函数的代码如下:

def main():
    msg = "Choose your mode! Enter 'init' or 'update_db_only' or 'update_all': "
    mode = input(msg)
    if mode.lower() == "init":
        setup_db()
    elif mode.lower() == "update_all":
        quotes_list = [fake.sentence() for _ in range(1, 11)]
        added = add_quotes(quotes_list)
        if added:
            print("New (fake) quotes added to the database:")
            for q in added:
                print(f"Added to DB: {q}")
                print("  - Also adding to the cache")
                cache.set(str(q[0]), q[1], ex=60)
    elif mode.lower() == "update_db_only":
        quotes_list = [fake.sentence() for _ in range(1, 11)]
        added = add_quotes(quotes_list)
        if added:
            print("New (fake) quotes added to the database ONLY:")
            for q in added:
                print(f"Added to DB: {q}")

那部分已经完成。现在,我们将创建另一个模块和脚本,用于缓存旁路相关的操作本身(在 ch08/cache_aside/cache_aside.py 文件中)。

我们这里也需要一些导入,然后是常量:

import sqlite3
from pathlib import Path
import redis
CACHE_KEY_PREFIX = "quote"
DB_PATH = Path(__file__).parent / Path("quotes.sqlite3")
cache = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)

接下来,我们定义一个 get_quote() 函数,通过标识符获取引语。如果我们不在缓存中找到引语,我们将查询数据库以获取它,并在返回之前将其放入缓存。函数定义如下:

def get_quote(quote_id: str) -> str:
    out = []
    quote = cache.get(f"{CACHE_KEY_PREFIX}.{quote_id}")
    if quote is None:
        # Get from the database
        query_fmt = "SELECT text FROM quotes WHERE id = {}"
        try:
            with sqlite3.connect(DB_PATH) as db:
                cursor = db.cursor()
                res = cursor.execute(query_fmt.format(quote_id)).fetchone()
                if not res:
                    return "There was no quote stored matching that id!"
                quote = res[0]
                out.append(f"Got '{quote}' FROM DB")
        except Exception as e:
            print(e)
            quote = ""
        # Add to the cache
        if quote:
            key = f"{CACHE_KEY_PREFIX}.{quote_id}"
            cache.set(key, quote, ex=60)
            out.append(f"Added TO CACHE, with key '{key}'")
    else:
        out.append(f"Got '{quote}' FROM CACHE")
    if out:
        return " - ".join(out)
    else:
        return ""

最后,在脚本的主体部分,我们要求用户输入一个引语标识符,并调用 get_quote() 来获取引语。代码如下:

def main():
    while True:
        quote_id = input("Enter the ID of the quote: ")
        if quote_id.isdigit():
            out = get_quote(quote_id)
            print(out)
        else:
            print("You must enter a number. Please retry.")

现在是测试我们脚本的时机,请按照以下步骤进行。

首先,通过调用 python ch08/cache_aside/populate_db.py 并选择 "init" 作为模式选项,我们可以看到在 ch08/cache_aside/ 文件夹中创建了一个 quotes.sqlite3 文件,因此我们可以得出结论,数据库已经创建,并在其中创建了一个 quotes 表。

然后,我们调用 python ch08/cache_aside/populate_db.py 并传递 update_all 模式;我们得到以下输出:

Choose your mode! Enter 'init' or 'update_db_only' or 'update_all': update_all
New (fake) quotes added to the database:
Added to DB: (62, 'Instead not here public.')
- Also adding to the cache
Added to DB: (26, 'Training degree crime serious beyond management and.')
- Also adding to the cache
Added to DB: (25, 'Agree hour example cover game bed.')
- Also adding to the cache
Added to DB: (23, 'Dark team exactly really wind.')
- Also adding to the cache
Added to DB: (46, 'Only loss simple born remain.')
- Also adding to the cache
Added to DB: (13, 'Clearly statement mean growth executive mean.')
- Also adding to the cache
Added to DB: (88, 'West policy a human job structure bed.')
- Also adding to the cache
Added to DB: (25, 'Work maybe back play.')
- Also adding to the cache
Added to DB: (18, 'Here certain require consumer strategy.')
- Also adding to the cache
Added to DB: (48, 'Discover method many by hotel.')
python ch08/cache_aside/populate_db.py and choose the update_db_only mode. In that case, we get the following output:

选择你的模式!输入 'init' 或 'update_db_only' 或 'update_all':update_db_only

仅向数据库中添加了新的(虚假的)引语:

添加到数据库中:(73,'Whose determine group what site.')

添加到数据库中:(77,'Standard much career either will when chance.')

添加到数据库中:(5,'Nature when event appear yeah.')

添加到数据库中:(81,'By himself in treat.')

添加到数据库中:(88,'Establish deal sometimes stage college everybody close thank.')

添加到数据库中:(99,'Room recently authority station relationship our knowledge occur.')

添加到数据库中:(63,'Price who a crime garden doctor eat.')

添加到数据库中:(43,'Significant hot those think heart shake ago.')

添加到数据库中:(80,'Understand and view happy.')

python ch08/cache_aside/cache_aside.py 命令,然后我们被要求输入一个尝试获取匹配引语的输入。以下是我根据提供的值得到的不同输出:

Enter the ID of the quote: 23
Got 'Dark team exactly really wind.' FROM DB - Added TO CACHE, with key 'quote.23'
Enter the ID of the quote: 12
There was no quote stored matching that id!
Enter the ID of the quote: 43
Got 'Significant hot those think heart shake ago.' FROM DB - Added TO CACHE, with key 'quote.43'
Enter the ID of the quote: 45
There was no quote stored matching that id!
Enter the ID of the quote: 77
Got 'Standard much career either will when chance.' FROM DB - Added TO CACHE, with key 'quote.77'
        因此,每次我输入一个与仅存储在数据库中的引语匹配的标识符(如前一个输出所示),具体的输出都显示数据首先从数据库中获取,然后从缓存(它立即被添加到其中)返回。

        我们可以看到一切按预期工作。缓存 aside 实现的更新部分(在数据库中写入条目并从缓存中删除相应的条目)留给你去尝试。你可以添加一个 `update_quote()` 函数,用于在传递 `quote_id` 给它时更新一个引语,并使用正确的命令行(例如 `python` `cache_aside.py update`)来调用它。

        记忆化模式

        **记忆化** 模式是软件开发中一个关键的优化技术,通过缓存昂贵函数调用的结果来提高程序的效率。这种方法确保了如果函数多次使用相同的输入被调用,则返回缓存的值,从而消除了重复和昂贵的计算需求。

        真实世界的例子

        我们可以将计算斐波那契数列视为记忆化模式的经典示例。通过存储序列之前计算过的值,算法避免了重新计算,这极大地加快了序列中更高数值的计算速度。

        另一个例子是文本搜索算法。在处理大量文本的应用中,如搜索引擎或文档分析工具,缓存先前搜索的结果意味着相同的查询可以立即返回结果,这显著提高了用户体验。

        记忆化模式的用例

        记忆化模式可以用于以下用例:

            1.  **加速递归算法**:记忆化将递归算法从具有高时间复杂度转变为低时间复杂度。这对于计算斐波那契数等算法特别有益。

            1.  **减少计算开销**:记忆化通过避免不必要的重新计算来节省 CPU 资源。这在资源受限的环境或处理大量数据处理时至关重要。

            1.  **提高应用性能**:记忆化的直接结果是应用性能的显著提升,使用户感觉应用响应更快、更高效。

        实现记忆化模式

        让我们讨论使用 Python 的 `functools.lru_cache` 装饰器实现记忆化模式的示例。这个工具对于具有昂贵计算且重复使用相同参数调用的函数特别有效。通过缓存结果,具有相同参数的后续调用将直接从缓存中检索结果,显著减少执行时间。

        对于我们的示例,我们将记忆化应用于一个经典问题,其中使用了递归算法:计算斐波那契数。

        我们首先需要以下 `import` 语句:
from datetime import timedelta
from functools import lru_cache
        第二,我们创建了一个名为 `fibonacci_func1` 的函数,该函数使用递归(不涉及任何缓存)来计算斐波那契数。我们将用它来进行比较:
def fibonacci_func1(n):
    if n < 2:
        return n
    return fibonacci_func1(n - 1) + fibonacci_func1(n - 2)
        第三,我们定义了一个名为 `fibonacci_func2` 的函数,代码与之前相同,但这次我们使用了 `lru_cache` 装饰器来启用记忆化。这里发生的情况是,函数调用的结果被存储在内存中的缓存中,具有相同参数的重复调用将直接从缓存中获取结果,而不是执行函数的代码。代码如下:
@lru_cache(maxsize=None)
def fibonacci_func2(n):
    if n < 2:
        return n
    return fibonacci_func2(n - 1) + fibonacci_func2(n - 2)
        最后,我们创建了一个 `main()` 函数来测试使用 `n=30` 作为输入调用两个函数,并测量每个执行的耗时。测试代码如下:
def main():
    import time
    n = 30
    start_time = time.time()
    result = fibonacci_func1(n)
    duration = timedelta(time.time() - start_time)
    print(f"Fibonacci_func1({n}) = {result}, calculated in {duration}")
    start_time = time.time()
    result = fibonacci_func2(n)
    duration = timedelta(time.time() - start_time)
    print(f"Fibonacci_func2({n}) = {result}, calculated in {duration}")
        要测试实现,请运行以下命令:`python ch08/memoization.py`。你应该得到以下输出:
Fibonacci_func1(30) = 832040, calculated in 7:38:53.090973
Fibonacci_func2(30) = 832040, calculated in 0:00:02.760315
        当然,你得到的时间可能与我不同,但使用缓存功能的第二个函数的时间应该短于没有缓存的函数的时间。而且,两者之间的时间差应该是重要的。

        这是一个演示,说明记忆化减少了计算斐波那契数所需的递归调用次数,尤其是对于大的`n`值。通过减少计算开销,记忆化不仅加快了计算速度,还节省了系统资源,从而使得应用程序更加高效和响应。

        懒加载模式

        **懒加载**模式是软件工程中的一个关键设计方法,尤其在优化性能和资源管理方面特别有用。懒加载的理念是在资源真正需要时才延迟初始化或加载资源。这样,应用程序可以实现更有效的资源利用,减少初始加载时间,并提升整体用户体验。

        真实世界的例子

        浏览在线艺术画廊提供了一个例子。网站不会一开始就加载数百张高分辨率图片,而是只加载当前视图中的图片。当你滚动时,额外的图片会无缝加载,从而提升你的浏览体验,而不会耗尽设备的内存或网络带宽。

        另一个例子是按需视频流媒体服务,如 Netflix 或 YouTube。这样的平台通过分块加载视频提供不间断的观看体验。这种方法不仅最小化了开始时的缓冲时间,还能适应不断变化的网络条件,确保视频质量一致,中断最少。

        在像 Microsoft Excel 或 Google Sheets 这样的应用程序中,处理大量数据集可能非常耗费资源。懒加载允许这些应用程序仅加载与当前视图或操作相关的数据,例如特定的工作表或单元格范围。这显著加快了操作速度并减少了内存使用。

        懒加载模式的用例

        我们可以将以下与性能相关的用例视为懒加载模式:

            1.  **减少初始加载时间**:这在网页开发中尤其有益,较短的加载时间可以转化为更高的用户参与度和留存率。

            1.  **保护系统资源**:在多样化的设备时代,从高端台式机到入门级智能手机,优化资源使用对于在所有平台上提供一致的用户体验至关重要。

            1.  **提升用户体验**:用户期望与软件进行快速、响应式的交互。懒加载通过最小化等待时间并使应用程序感觉更加响应来对此做出贡献。

        实现懒加载模式 – 懒属性加载

        考虑一个执行复杂数据分析或基于用户输入生成复杂可视化的应用程序。背后的计算是资源密集型和耗时的。在这种情况下实现懒加载可以显著提高性能。但为了演示目的,我们将不会像复杂的数据分析应用场景那样雄心勃勃。我们将使用一个模拟昂贵计算并返回用于类属性值的函数。

        对于这个懒加载示例,我们的想法是只有当属性第一次被访问时才初始化属性。这种方法在初始化属性是资源密集型,并且你希望推迟这个过程直到必要时常用的场景中。 

        我们从`LazyLoadedData`类的初始化部分开始,将`_data`属性设置为`None`。在这里,昂贵的资源尚未被加载:
class LazyLoadedData:
    def __init__(self):
        self._data = None
        我们添加了一个`data()`方法,使用`@property`装饰器,使其像属性(一个属性)一样工作,并添加了懒加载的逻辑。在这里,我们检查`_data`是否为`None`。如果是,我们调用`load_data()`方法:
    @property
    def data(self):
        if self._data is None:
            self._data = self.load_data()
        return self._data
        我们添加了一个`load_data()`方法,模拟一个昂贵的操作,使用`sum(i * i for i in range(100000))`。在现实世界的场景中,这可能涉及从远程数据库获取数据,执行复杂的计算或其他资源密集型任务:
    def load_data(self):
        print("Loading expensive data...")
        return sum(i * i for i in range(100000))
        然后我们添加一个`main()`函数来测试实现。我们创建`LazyLoadedData`类的一个实例,并两次访问`_data`属性:
def main():
    obj = LazyLoadedData()
    print("Object created, expensive attribute not loaded yet.")
    print("Accessing expensive attribute:")
    print(obj.data)
    print("Accessing expensive attribute again, no reloading occurs:")
    print(obj.data)
        要测试实现,运行`python ch08/lazy_loading/lazy_attribute_loading.py`命令。你应该得到以下输出:
Object created, expensive attribute not loaded yet.
Accessing expensive attribute:
Loading expensive data...
333328333350000
Accessing expensive attribute again, no reloading occurs:
_data. On subsequent accesses, the data stored is retrieved (from the attribute) without re-performing the expensive operation.
			The lazy loading pattern, applied this way, is very useful for improving performance in applications where certain data or computations are needed from time to time but are expensive to produce.
			Implementing the lazy loading pattern – using caching
			In this second example, we consider a function that calculates the factorial of a number using recursion, which can become quite expensive computationally as the input number grows. While Python’s `math` module provides a built-in function for calculating factorials efficiently, implementing it recursively serves as a good example of an expensive computation that could benefit from caching. We will use caching with `lru_cache`, as in the previous section, but this time for the purpose of lazy loading.
			We start with importing the modules and functions we need:

import time

from datetime import timedelta

from functools import lru_cache


			Then, we create a `recursive_factorial()` function that calculates the factorial of a number `n` recursively:

def recursive_factorial(n):

"""计算阶乘(对于大的 n 来说很昂贵)"""

if n == 1:

return 1

else:

return n * recursive_factorial(n - 1)


			Third, we create a `cached_factorial()` function that returns the result of calling `recursive_factorial()` and is decorated with `@lru_cache`. This way, if the function is called again with the same arguments, the result is retrieved from the cache instead of being recalculated, significantly reducing computation time:

@lru_cache(maxsize=128)

def cached_factorial(n):

return recursive_factorial(n)


			We create a `main()` function as usual for testing the functions. We call the non-cached function, and then we call the `cached_factorial` function twice, showing the computation time for each case. The code is as follows:

def main():

测试性能

n = 20

Without caching

start_time = time.time()

print(f"{ n }的递归阶乘:{ recursive_factorial(n) }")

duration = timedelta(time.time() - start_time)

print(f"无缓存的计算时间:{ duration }。")

With caching

start_time = time.time()

print(f"缓存的{ n }的阶乘:{ cached_factorial(n) }")

duration = timedelta(time.time() - start_time)

print(f"带缓存的计算时间:{ duration }。")

start_time = time.time()

print(f"缓存的{ n }的阶乘,重复:{ cached_factorial(n) }")

duration = timedelta(time.time() - start_time)

print(f"带缓存的第二次计算时间:{ duration }。")


			To test the implementation, run the `python ch08/lazy_loading/lazy_loading_with_caching.py` command. You should get the following output:

递归阶乘的 20:2432902008176640000

无缓存的计算时间:0:00:04.840851

缓存的 20 的阶乘:2432902008176640000

带缓存的计算时间:0:00:00.865173

缓存的 20 的阶乘,重复:2432902008176640000

带缓存的第二次计算时间:0:00:00.350189


			You will notice the time taken for the initial calculation of the factorial without caching, then the time with caching, and finally, the time for a repeated calculation with caching.
			Also, `lru_cache` is inherently a memoization tool, but it can be adapted and used in cases where, for example, there are expensive initialization processes that need to be executed only when required and not make the application slow. In our example, we used factorial computation to simulate such expensive processes.
			If you are asking yourself what is the difference from memoization, the answer is that the context in which caching is used here is for managing resource initialization.
			Summary
			Throughout this chapter, we have explored patterns that developers can use to enhance the efficiency and scalability of applications.
			The cache-aside pattern teaches us how to manage cache effectively, ensuring data is fetched and stored in a manner that optimizes performance and consistency, particularly in environments with dynamic data sources.
			The memoization pattern demonstrates the power of caching function results to speed up applications by avoiding redundant computations. This pattern is beneficial for expensive, repeatable operations and can dramatically improve the performance of recursive algorithms and complex calculations.
			Finally, the lazy loading pattern emphasizes delaying the initialization of resources until they are needed. This approach not only improves the startup time of applications but also reduces memory overhead, making it ideal for resource-intensive operations that may not always be necessary for the user’s interactions.
			In the next chapter, we are going to discuss patterns that govern distributed systems.


第九章:分布式系统模式

随着技术的进步和对可扩展和弹性系统的需求增加,了解支配分布式系统的基本模式变得至关重要。

从管理节点间的通信到确保容错性FT)和一致性,本章探讨了使开发者能够构建强大分布式系统的基本设计模式。无论你是构建微服务还是实施云原生应用程序,掌握这些模式都将为你提供有效应对分布式计算复杂性的工具。

在本章中,我们将涵盖以下主要主题:

  • 节流模式

  • 重试模式

  • 电路断路器模式

  • 其他分布式系统模式

技术要求

请参阅第一章中提出的需求。本章讨论的代码的附加技术要求如下:

  • 使用python -m pip install flask flask-limiter安装 Flask 和 Flask-Limiter

  • 使用python -m pip install pybreaker安装 PyBreaker

节流模式

节流是我们今天的应用程序和 API 中可能需要使用的重要模式。在这种情况下,节流意味着控制用户(或客户端服务)在给定时间内可以向特定服务或 API 发送请求的速率,以保护服务资源不被过度使用。例如,我们可能将 API 的用户请求限制为每天 1000 次。一旦达到这个限制,下一个请求将通过向用户发送包含 429 HTTP 状态代码的错误消息来处理,并告知用户请求过多。

关于节流有许多需要理解的内容,包括可能使用的限制策略和算法以及如何衡量服务的使用情况。你可以在微软的云设计模式目录中找到有关节流模式的详细技术信息(learn.microsoft.com/en-us/azure/architecture/patterns/throttling)。

现实世界示例

现实生活中有很多节流的例子,如下所示:

  • 高速公路交通管理:交通信号灯或限速规定调节高速公路上的车辆流量

  • 水龙头:调节水龙头的水流

  • 音乐会票销售:当热门音乐会的票开始销售时,网站可能会限制每个用户一次可以购买的票数,以防止服务器因需求激增而崩溃

  • 电力使用:一些公用事业公司提供基于高峰时段和非高峰时段电力使用情况的不同收费计划的方案

  • 自助餐队列:在自助餐中,为了确保每个人都有公平的机会进食并防止食物浪费,顾客可能一次只能取一份食物

我们还有帮助实现速率限制的软件片段的示例。

速率限制模式的用例

当你需要确保你的系统持续提供预期的服务,当你需要优化服务的使用成本,或者当你需要处理活动高峰时,这种模式是推荐的。

在实践中,你可以实施以下规则:

  • 将 API 的总请求数限制为每天 N 次(例如,N=1000)

  • 从给定的 IP 地址、国家或地区限制 API 的请求次数为每天 N 次

  • 限制认证用户的读写次数

除了速率限制的情况外,它还可以用于资源分配,确保在多个客户端之间公平分配资源。

实现速率限制模式

在深入实现示例之前,你需要知道存在几种类型的速率限制,其中包括速率限制、基于白名单 IP 地址列表的 IP 级别限制(例如)和并发连接限制,仅举这三个为例。前两种相对容易实验。我们将在这里重点关注第一种。

让我们看看使用 Flask 及其 Flask-Limiter 扩展开发的简单 Web 应用程序的速率限制类型速率限制的示例。

我们从示例所需的导入开始:

from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

与 Flask 一样,我们使用以下两行设置 Flask 应用程序:

app = Flask(__name__)

然后,我们定义 Limiter 实例;我们通过传递一个键函数get_remote_address(我们已导入)、应用程序对象、默认限制值和其他参数来创建它,如下所示:

limiter = Limiter(
    get_remote_address,
    app=app,
    default_limits=["100 per day", "10 per hour"],
    storage_uri="memory://",
    strategy="fixed-window",
)

基于此,我们可以为/limited路径定义一个路由,它将使用默认限制进行速率限制,如下所示:

@app.route("/limited")
def limited_api():
    return "Welcome to our API!"

我们还添加了/more_limited路径的路由定义。在这种情况下,我们使用@limiter.limit("2/minute")装饰器确保每分钟两个请求的速率限制。代码如下:

@app.route("/more_limited")
@limiter.limit("2/minute")
def more_limited_api():
    return "Welcome to our expensive, thus very limited, API!"

最后,我们添加了 Flask 应用程序中常用的片段:

if __name__ == "__main__":
    app.run(debug=True)

要测试此示例,请使用python ch09/throttling_flaskapp.py命令运行文件。你会得到一个启动的 Flask 应用程序的常规输出:

图 9.1 – throttling_flaskapp:Flask 应用程序示例启动

图 9.1 – throttling_flaskapp:Flask 应用程序示例启动

然后,如果你将浏览器指向http://127.0.0.1:5000/limited,你将看到页面上的欢迎内容,如下所示:

图 9.2 – 浏览器中/limited 端点的响应

图 9.2 – 浏览器中/limited 端点的响应

如果您继续点击刷新按钮,情况会变得有趣。第 10 次点击时,页面内容将改变并显示一个请求过多的错误消息,如下面的截图所示:

图 9.3 – 在/limited 端点上的请求过多

图 9.3 – 在/limited 端点上的请求过多

我们不要就此止步。记住——代码中还有第二条路由,即/more_limited,每分钟限制两个请求。为了测试第二条路由,将您的浏览器指向http://127.0.0.1:5000/more_limited。您将在页面上看到新的欢迎内容,如下所示:

图 9.4 – 浏览器中/more_limited 端点的响应

图 9.4 – 浏览器中/more_limited 端点的响应

如果我们点击刷新按钮,在 1 分钟内点击超过两次,我们将收到另一个请求过多的消息,如下面的截图所示:

图 9.5 – 在/more_limited 端点上的请求过多

图 9.5 – 在/more_limited 端点上的请求过多

此外,查看 Flask 服务器运行的控制台,您会注意到对每个接收到的 HTTP 请求和应用程序发送的响应状态码的提及。它应该看起来像下面的截图:

图 9.6 – Flask 服务器控制台:对 HTTP 请求的响应

图 9.6 – Flask 服务器控制台:对 HTTP 请求的响应

在使用 Flask-Limiter 扩展的 Flask 应用程序中,有许多速率限制类型节流的可能性,正如您可以在模块的文档页面中看到的那样。读者可以在文档页面上找到更多关于如何使用不同策略和存储后端(如 Redis)进行特定实现的信息。

重试模式

在分布式系统的背景下,重试是一种越来越需要的策略。想想微服务或基于云的基础设施,其中组件相互协作,但不是由同一团队和各方开发和部署/运营。

在其日常运营中,云原生应用程序的部分可能会遇到所谓的短暂故障或失败,这意味着一些看似错误但并非由于您的应用程序本身的问题;相反,它们是由于您无法控制的某些约束,如网络或外部服务器/服务的性能。因此,您的应用程序可能会出现故障(至少,这可能就是用户的感知)或在某些地方挂起。应对这种失败风险的答案是实施一些重试逻辑,这样我们就可以通过再次调用服务来通过问题,可能是立即或在等待一段时间(如几秒钟)之后。

现实世界示例

在我们的日常生活中,有许多重试模式(或类比)的例子,如下所示:

  • 打电话:想象你正在尝试通过电话联系一个朋友,但由于他们的线路繁忙或存在网络问题,电话无法接通。你不会立即放弃,而是在短暂的延迟后重试拨打他们的号码。

  • 从 ATM 取款:想象你前往 ATM 取现金,但由于网络拥塞或连接问题等暂时性问题,交易失败,机器显示错误信息。你不会放弃取款,而是稍作等待,再次尝试交易。这次,交易可能会成功,让你取出所需的现金。

在软件领域,也有许多工具或技术可以被视为示例,因为它们有助于实现重试模式,例如以下内容:

重试模式的用例

由于网络故障或服务器过载,与外部组件或服务通信时,此模式建议用于减轻已识别的暂时性故障的影响。

注意,重试方法不建议用于处理由应用程序逻辑本身错误引起的内部异常。此外,我们必须分析外部服务的响应。如果应用程序经常出现繁忙故障,这通常是一个迹象,表明被访问的服务存在需要解决的扩展问题。

我们可以将重试与微服务架构联系起来,其中服务通常通过网络进行通信。重试模式确保暂时性故障不会导致整个系统失败。

另一种用例是数据同步。当在两个系统之间同步数据时,重试可以处理一个系统的暂时不可用。

实现重试模式

在此示例中,我们将实现数据库连接的重试模式。我们将使用装饰器来处理重试机制。

我们从以下示例的import语句开始:

import logging
import random
import time

然后,我们添加配置以记录日志,这有助于在使用代码时的可观察性:

logging.basicConfig(level=logging.DEBUG)

我们添加我们的函数,该函数将支持装饰器自动重试被装饰函数的执行,直到达到指定的尝试次数,如下所示:

def retry(attempts):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(attempts):
                try:
                    logging.info("Retry happening")
                    return func(*args, **kwargs)
                except Exception as e:
                    time.sleep(1)
                    logging.debug(e)
            return "Failure after all attempts"
        return wrapper
    return decorator

然后,我们添加connect_to_database()函数,该函数模拟数据库连接。它被@retry装饰器装饰。我们希望装饰器在连接失败时自动重试连接,最多重试三次:

@retry(attempts=3)
def connect_to_database():
    if random.randint(0, 1):
        raise Exception("Temporary Database Error")
    return "Connected to Database"

最后,为了方便测试我们的实现,我们添加以下测试代码:

if __name__ == "__main__":
    for i in range(1, 6):
        logging.info(f"Connection attempt #{i}")
        print(f"--> {connect_to_database()}")

要测试示例,请运行以下命令:

python ch09/retry/retry_database_connection.py

你应该得到以下类似的输出:

INFO:root:Connection attempt #1
INFO:root:Retry happening
--> Connected to Database
INFO:root:Connection attempt #2
INFO:root:Retry happening
DEBUG:root:Temporary Database Error
INFO:root:Retry happening
DEBUG:root:Temporary Database Error
INFO:root:Retry happening
DEBUG:root:Temporary Database Error
--> Failure after all attempts
INFO:root:Connection attempt #3
INFO:root:Retry happening
--> Connected to Database
INFO:root:Connection attempt #4
INFO:root:Retry happening
--> Connected to Database
INFO:root:Connection attempt #5
INFO:root:Retry happening
DEBUG:root:Temporary Database Error
INFO:root:Retry happening
DEBUG:root:Temporary Database Error
INFO:root:Retry happening
DEBUG:root:Temporary Database Error
--> Failure after all attempts

因此,当临时数据库错误发生时,会进行重试。可能会发生几次重试,直到三次。一旦发生三次不成功的重试尝试,操作的结果就是失败。

总体而言,重试模式是处理涉及分布式系统的此类用例的有效方式,几个错误(例如我们示例中的四个数据库错误)可能意味着存在一个更永久或问题性的错误,应该修复。

断路器模式

实现 FT(故障转移)的一种方法涉及重试,正如我们刚才看到的。但是,当由于与外部组件通信导致的失败可能持续很长时间时,使用重试机制可能会影响应用程序的响应性。我们可能会浪费时间和资源尝试重复一个可能失败的请求。这就是另一个模式可以派上用场的地方:断路器模式。

使用断路器模式,你将一个脆弱的函数调用或与外部服务的集成点包装在一个特殊的(断路器)对象中,该对象会监控失败。一旦失败达到某个阈值,断路器就会跳闸,所有后续的断路器调用都会返回错误,而受保护的调用根本不会执行。

现实世界示例

在生活中,我们可以想到一个水或电力分配电路,其中断路器扮演着重要的角色。

在软件中,断路器在以下示例中使用:

  • 电子商务结账:如果支付网关关闭,断路器可以停止进一步的支付尝试,防止系统过载

  • 速率限制的 API:当一个 API 达到其速率限制时,断路器可以停止额外的请求以避免处罚

断路器模式的用例

如前所述,当你的系统需要与外部组件、服务或资源通信时,断路器模式在需要组件对长期失败具有容错能力时是推荐的。接下来,我们将了解它是如何解决这些用例的。

实现断路器模式

假设你想要在一个不可靠的函数上使用断路器,一个由于它依赖的网络环境而脆弱的函数,例如。我们将使用pybreaker库([pypi.org/project/pybreaker/](https://pypi.org/project/pybreaker/))来展示实现断路器模式的示例。

我们的实现是对这个仓库中找到的一个很好的脚本的改编:https://github.com/veltra/pybreaker-playground。让我们来看一下代码。

我们从导入开始,如下所示:

import pybreaker
from datetime import datetime
import random
from time import sleep

让我们定义我们的断路器,在函数中连续五次失败后自动打开电路。我们需要创建一个pybreaker.CircuitBreaker类的实例,如下所示:

breaker = pybreaker.CircuitBreaker(fail_max=2, reset_timeout=5)

然后,我们创建我们版本的函数来模拟脆弱的调用。我们使用装饰器语法来保护这些操作,因此新的函数如下所示:

@breaker
def fragile_function():
    if not random.choice([True, False]):
        print(" / OK", end="")
    else:
        print(" / FAIL", end="")
        raise Exception("This is a sample Exception")

最后,这是代码的主要部分,包括main()函数:

def main():
    while True:
        print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), end="")
        try:
            fragile_function()
        except Exception as e:
            print(" / {} {}".format(type(e), e), end="")
        finally:
            print("")
            sleep(1)

通过运行python ch09/circuit_breaker.py命令来调用此脚本会产生以下输出:

图 9.7 – 使用断路器的程序输出

图 9.7 – 使用断路器的程序输出

通过仔细查看输出,我们可以看到断路器按预期工作:当它打开时,所有对fragile_function()的调用都会立即失败(因为它们会引发CircuitBreakerError异常),而不会尝试执行预期的操作。并且,在 5 秒的超时后,断路器将允许下一个调用通过。如果该调用成功,电路将关闭;如果失败,电路将再次打开,直到另一个超时结束。

其他分布式系统模式

除了我们这里提到的模式之外,还有许多其他分布式系统模式。开发者和架构师可以使用的其他模式包括以下内容:

  • 命令和查询责任分离(CQRS):此模式将读取和写入数据的责任分开,通过针对特定用例定制数据模型和操作,优化数据访问和可扩展性。

  • 两阶段提交:此分布式事务协议通过协调两阶段提交过程,确保多个参与资源之间的原子性和一致性,包括一个准备阶段和一个随后的提交阶段。

  • 叙事:叙事是一系列本地事务的序列,这些事务共同构成一个分布式事务,提供补偿机制以在部分失败或事务中止的情况下保持一致性。

  • 边车:边车模式涉及在主要服务旁边部署额外的辅助服务以增强功能,例如添加监控、日志记录或安全功能,而无需直接修改主应用程序。

  • 服务注册表:此模式集中管理分布式系统内的服务发现,允许服务动态注册和发现彼此,从而促进通信和可扩展性。

  • 舱壁:受船舶设计启发,舱壁模式将系统内的资源或组件分区,以隔离故障并防止级联故障影响系统的其他部分,从而增强容错性和弹性。

这些模式中的每一个都针对分布式系统固有的特定挑战,为架构师和开发者提供策略和最佳实践,以设计能够在动态和不可预测的环境中运行的健壮和可扩展的解决方案。

摘要

在本章中,我们深入探讨了分布式系统模式的复杂性,重点关注节流、重试和断路器模式。这些模式对于构建健壮、容错和高效的分布式系统至关重要。

你在本章中获得的能力将显著提高你设计和实现能够处理短暂故障、服务中断和高负载的分布式系统的能力。

关于节流模式的章节为你提供了有效管理服务负载和资源分配的工具。

通过理解如何实现重试模式,你已经掌握了使你的操作更可靠的技能。

最后,断路器模式教会了你如何构建能够优雅处理故障的容错系统。

当我们结束本章时,记住这些模式不是孤立的解决方案,而是更大拼图的一部分。它们通常在结合并针对你系统的特定需求和限制进行调整时效果最佳。关键是要理解其背后的原则,这样你就可以根据需要调整它们,以创建一个有弹性和高效的分布式系统。

最后,我们简要介绍了其他一些分布式系统模式,这些模式我们无法在本书中涵盖。

在下一章中,我们将专注于测试模式。

第十章:测试模式

在前面的章节中,我们介绍了架构模式和针对特定用例(如并发或性能)的模式。

在本章中,我们将探讨特别适用于测试的设计模式。这些模式有助于隔离组件,使测试更加可靠,并促进代码重用。

在本章中,我们将介绍以下主要主题:

  • 模拟对象模式

  • 依赖注入模式

技术要求

请参阅在第一章中提出的需求。

模拟对象模式

模拟对象模式是一种在测试期间通过模拟其行为来隔离组件的强大工具。模拟对象有助于创建受控的测试环境并验证组件之间的交互。

模拟对象模式提供了三个功能:

  1. 隔离:模拟将正在测试的代码单元隔离,确保测试在受控环境中运行,其中依赖项是可预测的,并且没有外部副作用。

  2. 行为验证:通过使用模拟对象,您可以在测试期间验证某些行为是否发生,例如方法调用或属性访问。

  3. 简化:它们通过替换可能需要大量设置的复杂真实对象来简化测试的设置。

与存根的比较

存根也替换了真实实现,但仅用于向被测试的代码提供间接输入。相比之下,模拟可以验证交互,使它们在许多测试场景中更加灵活。

现实世界示例

我们可以想到以下现实世界的类比概念或工具:

  • 飞行模拟器,这是一种旨在复制实际驾驶飞机体验的工具。它允许飞行员在受控和安全的环境中学习如何处理各种飞行场景。

  • 心肺复苏CPR)模拟人,用于教授学生如何有效地进行心肺复苏。它模拟人体以提供一个真实但受控的学习环境。

  • 碰撞测试模拟人,由汽车制造商用于模拟人类对车辆碰撞的反应。它提供了关于汽车碰撞影响和安全特性的宝贵数据,而无需冒实际人类生命危险。

模拟对象模式的用例

单元测试中,模拟对象用于替换被测试代码的复杂、不可靠或不可用的依赖项。这允许开发者仅关注单元本身,而不是它与外部系统的交互。例如,当测试一个从 API 获取数据的服务时,模拟对象可以通过返回预定义的响应来模拟 API,确保服务能够处理各种数据场景或错误,而无需与实际 API 交互。

虽然与单元测试类似,但使用模拟对象的集成测试侧重于组件之间的交互,而不是单个单元。模拟可以用来模拟尚未开发或成本过高而无法参与每个测试的组件。例如,在微服务架构中,模拟可以代表一个正在开发或暂时不可用的服务,允许其他服务在如何集成和与其通信方面进行测试。

模拟对象模式对于行为验证也非常有用。此用例涉及验证对象之间是否发生预期的某些交互。模拟对象可以被编程为期望特定的调用、参数甚至交互顺序,这使得它们成为行为测试的强大工具;例如,测试控制器在模型-视图-控制器MVC)架构中在处理用户请求之前是否正确地调用了身份验证和日志记录服务。模拟可以验证控制器是否以正确的顺序进行了正确的调用,例如在尝试记录请求之前检查凭证。

实现模拟对象模式

假设我们有一个将消息记录到文件的函数。我们可以模拟文件写入机制,以确保我们的日志记录函数将预期的内容写入日志,而不写入文件。让我们看看如何使用 Python 的unittest模块来实现这一点。

首先,我们导入示例中需要的模块:

import unittest
from unittest.mock import mock_open, patch

然后,我们创建一个表示简单日志记录器的类,该记录器将消息写入初始化期间指定的文件:

class Logger:
    def __init__(self, filepath):
        self.filepath = filepath
    def log(self, message):
        with open(self.filepath, "a") as file:
            file.write(f"{message}\n")

接下来,我们创建一个继承自unittest.TestCase类的测试用例类,就像通常一样。在这个类中,我们需要test_log()方法来测试日志记录器的log()方法,如下所示:

class TestLogger(unittest.TestCase):
    def test_log(self):
        msg = "Hello, logging world!"

接下来,我们将在测试范围内直接模拟 Python 内置的open()函数。模拟函数是通过使用unittest.mock.patch()来完成的,它临时用模拟对象(调用mock_open()的结果)替换了目标对象,即builtins.open。通过调用unittest.mock.patch()函数获得的上下文管理器,我们创建一个Logger对象并调用其.log()方法,这应该会触发open()函数:

        m_open = mock_open()
        with patch("builtins.open", m_open):
            logger = Logger("dummy.log")
            logger.log(msg)

关于builtins

根据 Python 文档,builtins模块提供了对 Python 所有内置标识符的直接访问;例如,builtins.openopen()内置函数的全名。见docs.python.org/3/library/builtins.html

关于mock_open

当你调用mock_open()时,它返回一个配置为像内置的open()函数一样行为的 Mock 对象。此模拟被设置为模拟文件操作,如读取和写入。

关于unittest.mock.patch

它用于在测试期间用模拟对象替换对象。它的参数包括 target,用于指定要替换的对象,以及可选参数:new 用于可选的替换对象,specautospec 用于将模拟限制在真实对象的属性上以提高准确性,spec_set 用于更严格的属性指定,side_effect 用于定义条件行为或异常,return_value 用于设置固定的响应,wraps 用于在修改某些方面时允许原始对象的行为。这些选项使测试场景中的精确控制和灵活性成为可能。

现在,我们检查日志文件是否正确打开,我们使用两种验证方法来完成。对于第一个验证,我们在模拟对象上使用 assert_called_once_with() 方法,以检查 open() 函数是否以预期的参数被调用。对于第二个验证,我们需要从 unittest.mock.mock_open 中获取更多技巧;我们的 m_open 模拟对象,通过调用 mock_open() 函数获得,也是一个可调用对象,每次被调用时都像是一个创建新模拟文件句柄的工厂。我们使用它来获取一个新的文件句柄,然后在该文件句柄上的 write() 方法调用上使用 assert_called_once_with(),这有助于我们检查 write() 方法是否以正确的消息被调用。测试函数的这一部分如下:

            m_open.assert_called_once_with(
                "dummy.log", "a"
            )
            m_open().write.assert_called_once_with(
                f"{msg}\n"
            )

最后,我们调用 unitest.main()

if __name__ == "__main__":
    unittest.main()

要执行示例(在 ch10/mock_object.py 文件中),像往常一样,运行以下命令:

python ch10/mock_object.py

你应该得到以下输出:

.
---------------------------------------------------------
Ran 1 test in 0.012s
OK

这只是一个快速演示,展示了如何在单元测试中使用模拟来模拟系统的一部分。我们可以看到,这种方法隔离了副作用(即文件 I/O),确保单元测试不会创建或需要实际文件。它允许测试类的内部行为,而不需要为了测试目的而改变类的结构。

依赖注入模式

依赖注入模式涉及将类的依赖项作为外部实体传递,而不是在类内创建它们。这促进了松散耦合、模块化和可测试性。

现实世界的例子

在现实生活中,我们会遇到以下例子:

  • 电器和电源插座:各种电器可以插入不同的电源插座,使用电力而无需直接和永久布线

  • 相机镜头:摄影师可以在不改变相机本身的情况下,根据不同的环境和需求更换相机的镜头

  • 模块化列车系统:在模块化列车系统中,可以根据每次旅行的需求添加或移除单个车厢(如卧铺车厢、餐厅车厢或行李车厢)

依赖注入模式的用例

在 Web 应用程序中,将数据库连接对象注入到组件(如仓库或服务)中,可以增强模块化和可维护性。这种做法允许轻松地在不同的数据库引擎或配置之间切换,而无需直接修改组件的代码。它还通过允许注入模拟数据库连接,从而简化了单元测试过程,从而在不影响实时数据库的情况下测试各种数据场景。

另一种使用场景是管理跨各种环境(开发、测试、生产等)的配置设置。通过动态将设置注入到模块中,依赖注入DI)减少了模块与其配置源之间的耦合。这种灵活性使得在不进行大量重新配置的情况下,更容易管理和切换环境。在单元测试中,这意味着你可以注入特定的设置来测试模块在不同配置下的表现,确保其健壮性和功能。

实现依赖注入模式 - 使用模拟对象

在这个第一个例子中,我们将创建一个简单的场景,其中WeatherService类依赖于WeatherApiClient接口来获取天气数据。对于示例的单元测试代码,我们将注入该 API 客户端的模拟版本。

我们首先定义任何天气 API 客户端实现应遵守的接口,使用 Python 的Protocol功能:

from typing import Protocol
class WeatherApiClient(Protocol):
    def fetch_weather(self, location):
        """Fetch weather data for a given location"""
        ...

然后,我们添加一个RealWeatherApiClient类,该类实现了该接口,并将与我们的天气服务进行交互。在实际场景中,在提供的fetch_weather()方法中,我们会调用天气服务,但为了使示例简单并专注于本章的主要概念;所以我们提供了一个模拟,简单地返回一个表示天气数据结果的字符串。代码如下:

class RealWeatherApiClient:
    def fetch_weather(self, location):
        return f"Real weather data for {location}"

接下来,我们创建一个天气服务,它使用实现WeatherApiClient接口的对象来获取天气数据:

class WeatherService:
    def __init__(self, weather_api: WeatherApiClient):
        self.weather_api = weather_api
    def get_weather(self, location):
        return self.weather_api.fetch_weather(location)

最后,我们准备好通过WeatherService构造函数注入 API 客户端的依赖。我们添加代码来帮助手动测试示例,使用以下真实服务:

if __name__ == "__main__":
    ws = WeatherService(RealWeatherApiClient())
    print(ws.get_weather("Paris"))

在我们的示例的这一部分(在ch10/dependency_injection/di_with_mock.py文件中)可以通过以下命令手动测试:

python ch10/dependency_injection/di_with_mock.py

你应该得到以下输出:

ch10/dependency_injection/test_di_with_mock.py).
			First, we import the `unittest` module, as well as the `WeatherService` class (from our `di_with_mock` module), as follows:

导入 unittest 模块

from di_with_mock import WeatherService


			Then, we create a mock version of the weather API client implementation that will be useful for unit testing, simulating responses without making real API calls:

class MockWeatherApiClient:

def fetch_weather(self, location):

return f"为 {location} 的模拟天气数据"


			Next, we write the test case class, with a test function. In that function, we inject the mock API client instead of the real API client, passing it to the `WeatherService` constructor, as follows:

class TestWeatherService(unittest.TestCase):

def test_get_weather(self):

mock_api = MockWeatherApiClient()

weather_service = WeatherService(mock_api)

self.assertEqual(

weather_service.get_weather("Anywhere"),

"为任何地方的模拟天气数据",

)


			We finish by adding the usual lines for executing unit tests when the file is interpreted by Python:

if name == "main":

unittest.main()


			Executing this part of the example (in the `ch10/dependency_injection/test_di_with_mock.py` file), using the `python ch10/dependency_injection/test_di_with_mock.py` command, gives the following output:

.


执行了 1 个测试,耗时 0.000 秒

OK


			The test with the dependency injected using a mock object succeeded.
			Through this example, we were able to see that the `WeatherService` class doesn’t need to know whether it’s using a real or a mock API client, making the system more modular and easier to test.
			Implementing the Dependency Injection pattern – using a decorator
			It is also possible to use decorators for DI, which simplifies the injection process. Let’s see a simple example demonstrating how to do that, where we’ll create a notification system that can send notifications through different channels (for example, email or SMS). The first part of the example will show the result based on manual testing, and the second part will provide unit tests.
			First, we define a `NotificationSender` interface, outlining the methods any notification sender should have:

from typing import Protocol

class NotificationSender(Protocol):

def send(self, message: str):

"""使用给定消息发送通知"""

...


			Then, we implement two specific notification senders: the `EmailSender` class implements sending a notification using email, and the `SMSSender` class implements sending using SMS. This part of the code is as follows:

class EmailSender:

def send(self, message: str):

打印(f"发送电子邮件:{message}")

class SMSSender:

def send(self, message: str):

打印(f"发送短信:{message}")


			We also define a notification service class, `NotificationService`, with a class attribute sender and a `.notify()` method, which takes in a message and calls `.send()` on the provided sender object to send the message, as follows:

class NotificationService:

sender: NotificationSender = None

def notify(self, message):

self.sender.send(message)


			What is missing is the decorator that will operate the DI, to provide the specific sender object to be used. We create our decorator to decorate the `NotificationService` class for injecting the sender. It will be used by calling `@inject_sender(EmailSender)` if we want to inject the email sender, or `@inject_sender(SMSSender)` if we want to inject the SMS sender. The code for the decorator is as follows:

def inject_sender(sender_cls):

def decorator(cls):

cls.sender = sender_cls()

return cls

return decorator


			Now, if we come back to the notification service’s class, the code would be as follows:

@inject_sender(EmailSender)

class NotificationService:

sender: NotificationSender = None

def notify(self, message):

self.sender.send(message)


			Finally, we can instantiate the `NotificationService` class in our application and notify a message for testing the implementation, as follows:

if name == "main":

service = NotificationService()

service.notify("Hello, this is a test notification!")


			That first part of our example (in the `ch10/dependency_injection/di_with_decorator.py` file) can be manually tested by using the following command:

python ch10/dependency_injection/di_with_decorator.py


			You should get the following output:

发送电子邮件:您好,这是一条测试通知!


			If you change the decorating line, replace the `EmailSender` class with `SMSSender`, and rerun that command, you will get the following output:

发送短信:您好,这是一条测试通知!


			That shows the DI is effective.
			Next, we want to write unit tests for that implementation. We could use the mocking technique, but to see other ways, we are going to use the stub classes approach. The stubs manually implement the dependency interfaces and include additional mechanisms to verify that methods have been called correctly. Let’s start by importing what we need:

import unittest

from di_with_decorator import (

NotificationSender,

NotificationService,

inject_sender,

)


			Then, we create stub classes that implement the `NotificationSender` interface. These classes will help record calls to their `send()` method, using the `messages_sent` attribute on their instances, allowing us to check whether the correct methods were called during the test. Both stub classes are as follows:

class EmailSenderStub:

def init(self):

self.messages_sent = []

def send(self, message: str):

self.messages_sent.append(message)

class SMSSenderStub:

def init(self):

self.messages_sent = []

def send(self, message: str):

self.messages_sent.append(message)


			Next, we are going to use both stubs in our test case to verify the functionality of `NotificationService`. In the test function, `test_notify_with_email`, we create an instance of `EmailSenderStub`, inject that stub into the service, send a notification message, and then verify that the message was sent by the email stub. That part of the code is as follows:

class TestNotifService(unittest.TestCase):

def test_notify_with_email(self):

email_stub = EmailSenderStub()

service = NotificationService()

service.sender = email_stub

service.notify("Test Email Message")

self.assertIn(

"Test Email Message",

email_stub.messages_sent,

)


			We need another function for the notification with SMS functionality, `test_notify_with_sms`. Similarly to the previous case, we create an instance of `SMSSenderStub`. Then, we need to inject that stub into the notification service. But, for that, in the scope of the test, we define a custom notification service class, and decorate it with `@inject_sender(SMSSenderStub)`, as follows:

@inject_sender(SMSSenderStub)

class CustomNotificationService:

sender: NotificationSender = None

def notify(self, message):

self.sender.send(message)


			Based on that, we inject the SMS sender stub into the custom service, send a notification message, and then verify that the message was sent by the SMS stub. The complete code for the second unit test is as follows:

def test_notify_with_sms(self):

sms_stub = SMSSenderStub()

@inject_sender(SMSSenderStub)

class CustomNotificationService:

sender: NotificationSender = None

def notify(self, message):

self.sender.send(message)

service = CustomNotificationService()

service.sender = sms_stub

service.notify("Test SMS Message")

self.assertIn(

"Test SMS Message", sms_stub.messages_sent

)


			Finally, we should not forget to add the lines needed for executing unit tests when the file is interpreted by Python:

if name == "main":

unittest.main()


			Executing the unit test code (in the `ch10/dependency_injection/test_di_with_decorator.py` file), using the `python ch10/dependency_injection/test_di_with_decorator.py` command, gives the following output:

..


测试完成,运行了 2 个测试用例,耗时 0.000 秒

OK


			This is what was expected.
			So, this example showed how using a decorator to manage dependencies allows for easy changes without modifying the class internals, which not only keeps the application flexible but also encapsulates the dependency management outside of the core business logic of your application. In addition, we saw how DI can be tested with unit tests using the stubs technique, ensuring the application’s components work as expected in isolation.
			Summary
			In this chapter, we’ve explored two pivotal patterns essential for writing clean code and enhancing our testing strategies: the Mock Object pattern and the Dependency Injection pattern.
			The Mock Object pattern is crucial for ensuring test isolation, which helps avoid unwanted side effects. It also facilitates behavior verification and simplifies test setup. We discussed how mocking, particularly through the `unittest.mock` module, allows us to simulate components within a unit test, demonstrating this with a practical example.
			The Dependency Injection pattern, on the other hand, offers a robust framework for managing dependencies in a way that enhances flexibility, testability, and maintainability. It’s applicable not only in testing scenarios but also in general software design. We illustrated this pattern with an initial example that integrates mocking for either unit or integration tests. Subsequently, we explored a more advanced implementation using a decorator to streamline dependency management across both the application and its tests.
			As we conclude this chapter and prepare to enter the final one, we’ll shift our focus slightly to discuss Python anti-patterns, identifying common pitfalls, and learning how to avoid them.

第十一章:Python 反模式

在本章的最后部分,我们将探讨 Python 反模式。这些是常见的编程实践,虽然它们并不一定错误,但往往会导致代码效率低下、可读性差和难以维护。通过了解这些陷阱,你可以为你的 Python 应用程序编写更干净、更高效的代码。

在本章中,我们将涵盖以下主要内容:

  • 代码风格违规

  • 正确性反模式

  • 可维护性反模式

  • 性能反模式

技术要求

请参阅在第一章中提出的要求数据。

代码风格违规

Python 风格指南,也称为Python 增强提案第 8 号PEP 8),为你的代码的可读性和一致性提供了建议,使得开发者能够更容易地在长时间内协作和维护项目。你可以在其官方页面找到风格指南的详细信息:peps.python.org/pep-0008。在本节中,我们将介绍风格指南的一些建议,以便你在编写应用程序或库的代码时避免它们。

修复代码风格违规的工具

注意,我们有如 Black (black.readthedocs.io/en/stable/)、isort (pycqa.github.io/isort/) 和/或 Ruff (docs.astral.sh/ruff/) 等格式化工具,可以帮助你修复不符合风格指南建议的代码。我们不会在这里花费时间讲解如何使用这些工具,因为你可以找到所有需要的文档在它们的官方文档页面上,并且可以在几分钟内开始使用它们。

现在,让我们来探讨我们选定的代码风格建议。

缩进

你应该使用每个缩进级别四个空格,并应避免混合使用制表符和空格。

最大行长度和空行

风格指南建议将所有代码行限制在最多 79 个字符,以提高可读性。

此外,还有一些与空行相关的规则。首先,你应该在顶级函数和类定义周围使用两个空行。其次,类内部的方法定义应使用单个空行。

例如,以下代码片段的格式是不正确的:

class MyClass:
    def method1(self):
        pass
    def method2(self):
        pass
def top_level_function():
    pass

正确的格式如下:

class MyClass:
    def method1(self):
        pass
    def method2(self):
        pass
def top_level_function():
    pass

导入

你编写、组织和排序导入语句的方式也很重要。根据风格指南,导入应单独成行,并按以下顺序分为三类:标准库导入、相关第三方库导入以及应用程序或库代码库中的本地特定导入。此外,每个组之间应有一个空行。

例如,以下内容不符合风格指南:

import os, sys
import numpy as np
from mymodule import myfunction

对于相同的导入,最佳实践如下:

import os
import sys
import numpy as np
from mymodule import myfunction

命名约定

你应该为变量、函数、类和模块使用描述性的名称。以下是为不同类型情况的具体命名约定:

  • lower_case_with_underscores

  • CapWords

  • ALL_CAPS_WITH_UNDERSCORES

例如,以下不是良好的实践:

def calculateSum(a, b):
    return a + b
class my_class:
    pass
maxValue = 100

最佳实践如下:

def calculate_sum(a, b):
    return a + b
class MyClass:
    pass
MAX_VALUE = 100

注释

注释应该是完整的句子,首字母大写,应该清晰简洁。我们对注释的两种情况有具体的建议——块注释和行内注释:

  • 块注释通常适用于其后的某些(或所有)代码,并且缩进与该代码相同级别。块注释的每一行都以 # 和一个空格开始。

  • 行内注释应谨慎使用。行内注释位于语句的同一行上,至少与语句有两个空格的距离。

例如,以下是一个不良的注释风格:

#This is a poorly formatted block comment.
def foo():  #This is a poorly formatted inline comment.
    pass

下面是修复了风格的等效代码:

# This is a block comment.
# It spans multiple lines.
def foo():
    pass  # This is an inline comment.

表达式和语句中的空白

在以下情况下,你应该避免多余的空白:

  • 立即位于括号、方括号或花括号内

  • 立即位于逗号、分号或冒号之前

  • 在赋值运算符周围留出多个空格以对齐

这结束了我们对最常见代码风格违规的审查。正如之前所说,有工具可以帮助以生产性的方式检测和修复这些违规,它们通常包含在开发工作流程中(例如,通过 git commit 钩子以及/或在项目的 CI/CD 流程中)。

正确性反模式

如果不解决这些问题,这些反模式可能会导致错误或意外的行为。我们将讨论这些反模式中最常见的一些,以及推荐的替代方法和途径。我们将重点关注以下反模式:

  • 使用 type() 函数比较类型

  • 可变默认参数

  • 从类外部访问受保护的成员

注意,使用如 Visual Studio CodePyCharm 这样的 IDE 或如 Flake8 这样的命令行工具可以帮助你在代码中找到这样的不良实践,但了解每个建议及其背后的原因同样重要。

使用 type() 函数比较类型

有时,为了我们的算法,我们需要通过比较来识别值的类型。人们可能会想到的常见技术是使用 type() 函数。但使用 type() 来比较对象类型不考虑子类化,并且不如基于使用 isinstance() 函数的替代方案灵活。

假设我们有两个类,CustomListACustomListB,它们是 UserList 类的子类,当定义自定义列表的类时,推荐从该类继承,如下所示:

from collections import UserList
class CustomListA(UserList):
    pass
class CustomListB(UserList):
    pass

如果我们想检查一个对象是否是自定义列表类型之一,使用第一种方法,我们会测试 type(obj) in (CustomListA, CustomListB) 条件。

或者,我们只需简单地测试isinstance(obj, UserList),这已经足够了,因为CustomListACustomListBUserList的子类。

作为演示,我们编写一个compare()函数,使用以下第一种方法:

def compare(obj):
    if type(obj) in (CustomListA, CustomListB):
        print("It's a custom list!")
    else:
        print("It's a something else!")

然后,我们编写一个better_compare()函数,使用以下替代方法执行等效操作:

def better_compare(obj):
    if isinstance(obj, UserList):
        print("It's a custom list!")
    else:
        print("It's a something else!")

以下代码行可以帮助测试这两个函数:

obj1 = CustomListA([1, 2, 3])
obj2 = CustomListB(["a", "b", "c"])
compare(obj1)
compare(obj2)
better_compare(obj1)
better_compare(obj2)

完整的演示代码在ch11/compare_types.py文件中。运行python ch11/compare_types.py命令应给出以下输出:

It's a custom list!
It's a custom list!
It's a custom list!
It's a custom list!

这表明两个函数都可以产生预期的结果。但使用推荐技术isinstance()的函数更简单易写,并且更灵活,因为它考虑了子类。

可变默认参数

当你定义一个带有期望可变值参数的函数,例如列表或字典时,你可能想提供一个默认参数(分别为[]{})。但这样的函数会在调用之间保留更改,这会导致意外的行为。

建议的做法是使用None作为默认值,并在需要时在函数内部将其设置为可变数据结构。

让我们创建一个名为manipulate()的函数,其mylist参数的默认值为[]。该函数将"test"字符串追加到mylist列表中,然后返回它,如下所示:

def manipulate(mylist=[]):
    mylist.append("test")
    return mylist

在另一个名为better_manipulate()的函数中,其mylist参数的默认值为None,我们首先将mylist设置为[],如果它是None,然后在返回之前将"test"字符串追加到mylist中,如下所示:

def better_manipulate(mylist=None):
    if not mylist:
        mylist = []
    mylist.append("test")
    return mylist

以下行帮助我们通过多次使用默认参数调用每个函数来测试每个函数:

if __name__ == "__main__":
    print("function manipulate()")
    print(manipulate())
    print(manipulate())
    print(manipulate())
    print("function better_manipulate()")
    print(better_manipulate())
    print(better_manipulate())

运行python ch11/mutable_default_argument.py命令应给出以下输出:

function manipulate()
['test']
['test', 'test']
['test', 'test', 'test']
function better_manipulate()
['test']
"test" string several times in the list returned; the string is accumulating because each subsequent time the function has been called, the mylist argument kept its previous value instead of being reset to the empty list. But, with the recommended solution, we see with the result that we get the expected behavior.
			Accessing a protected member from outside a class
			Accessing a protected member (an attribute prefixed with `_`) of a class from outside that class usually calls for trouble since the creator of that class did not intend this member to be exposed. Someone maintaining the code could change or rename that attribute later down the road, and parts of the code accessing it could result in unexpected behavior.
			If you have code that accesses a protected member that way, the recommended practice is to refactor that code so that it is part of the public interface of the class.
			To demonstrate this, let’s define a `Book` class with two protected attributes, `_title` and `_author`, as follows:

class Book:

def init(self, title, author):

self._title = title

self._author = author


			Now, let’s create another class, `BetterBook`, with the same attributes and a `presentation_line()` method that accesses the `_title` and `_author` attributes and returns a concatenated string based on them. The class definition is as follows:

class BetterBook:

def init(self, title, author):

self._title = title

self._author = author

def presentation_line(self):

return f"{self._title} by {self._author}"


			Finally, in the code for testing both classes, we get and print the presentation line for an instance of each class, accessing the protected members for the first one (instance of `Book`) and calling the `presentation_line()` method for the second one (instance of `BetterBook`), as follows:

if name == "main":

b1 = Book(

"Mastering Object-Oriented Python",

"Steven F. Lott",

)

print(

"不良做法:直接访问受保护的成员"

)

print(f"{b1._title} by {b1._author}")

b2 = BetterBook(

"Python 算法",

"Magnus Lie Hetland",

)

print(

"推荐:通过公共接口访问"

)

print(b2.presentation_line())


			The complete code is in the `ch11/ protected_member_of_class.py` file. Running the `python ch11/ protected_member_of_class.py` command gives the following output:

不良做法:直接访问受保护的成员

《精通面向对象 Python》由 Steven F. Lott 著

推荐:通过公共接口访问

"Python Algorithms" by Magnus Lie Hetland


			This shows that we get the same result, without any error, in both cases, but using the `presentation_line()` method, as done in the case of the second class, is the best practice. The `_title` and `_author` attributes are protected, so it is not recommended to call them directly. The developer could change those attributes in the future. That is why they must be encapsulated in a public method.
			Also, it is good practice to provide an attribute that encapsulates each protected member of the class using the `@property` decorator, as we have seen in the *Techniques for achieving encapsulation* section of *Chapter 1*, *Foundational* *Design Principles*.
			Maintainability anti-patterns
			These anti-patterns make your code difficult to understand or maintain over time. We are going to discuss several anti-patterns that should be avoided for better quality in your Python application or library’s code base. We will focus on the following points:

				*   Using a wildcard import
				*   **Look Before You Leap** (**LBYL**) versus **Easier to Ask for Forgiveness than** **Permission** (**EAFP**)
				*   Overusing inheritance and tight coupling
				*   Using global variables for sharing data between functions

			As mentioned for the previous category of anti-patterns, using tools such as Flake8 as part of your developer workflow can be handy to help find some of those potential issues when they are already present in your code.
			Using a wildcard import
			This way of importing (`from mymodule import *`) can clutter the namespace and make it difficult to determine where an imported variable or function came from. Also, the code may end up with bugs because of name collision.
			The best practice is to use specific imports or import the module itself to maintain clarity.
			LBYL versus EAFP
			LBYL often leads to more cluttered code, while EAFP makes use of Python’s handling of exceptions and tends to be cleaner.
			For example, we may want to check if a file exists, before opening it, with code such as the following:

if os.path.exists(filename):

with open(filename) as f:

print(f.text)


			This is LBYL, and when new to Python, you would think that it is the right way to treat such situations. But in Python, it is recommended to favor EAFP, where appropriate, for cleaner, more Pythonic code. So, the recommended way for the expected result would give the following code:

try:

with open(filename) as f:

print(f.text)

except FileNotFoundError:

print("此处无文件")


			As a demonstration, let’s write a `test_open_file()` function that uses the LBYL approach, as follows:

def test_open_file(filename):

if os.path.exists(filename):

with open(filename) as f:

print(f.text)

else:

print("此处无文件")


			Then, we add a function that uses the recommended approach:

def better_test_open_file(filename):

try:

with open(filename) as f:

print(f.text)

except FileNotFoundError:

print("No file there")


			We can then test these functions with the following code:

filename = "no_file.txt"

test_open_file(filename)

better_test_open_file(filename)


			You can check the complete code of the example in the `ch11/lbyl_vs_eafp.py` file, and running it should give the following output:

没有该文件

try/except 方法使我们的代码更简洁。

        过度使用继承和紧密耦合

        继承是面向对象编程的一个强大功能,但过度使用它——例如,为每个轻微的行为变化创建一个新的类——会导致类之间的紧密耦合。这增加了复杂性,并使代码更不灵活,更难以维护。

        不推荐创建如下的深层继承层次结构(作为一个简化的例子):
class GrandParent:
    pass
class Parent(GrandParent):
    pass
class Child(Parent):
    Pass
        最佳实践是创建更小、更专注的类,并将它们组合起来以实现所需的行为,如下所示:
class Parent:
    pass
class Child:
    def __init__(self, parent):
        self.parent = parent
        如您所记得,这是组合方法,我们在 *第一章* 的 *遵循组合优于继承原则* 部分进行了讨论,*基础* *设计原则*。

        使用全局变量在函数之间共享数据

        全局变量是可以在整个程序中访问的变量,这使得它们在函数之间共享数据时很有吸引力——例如,跨多个模块使用的配置设置或共享资源,如数据库连接。

        然而,它们可能导致应用程序的不同部分意外地修改全局状态,从而导致错误。此外,它们使得扩展应用程序变得更加困难,因为它们可能导致多线程环境中的问题,在多线程环境中,多个线程可能会尝试同时修改全局变量。

        下面是一个不推荐的做法示例:
# Global variable
counter = 0
def increment():
    global counter
    counter += 1
def reset():
    global counter
    counter = 0
        而不是使用全局变量,你应该将所需的数据作为参数传递给函数或封装状态在类中,这提高了代码的模块化和可测试性。因此,对于反例的最佳实践是定义一个包含 `counter` 属性的 `Counter` 类,如下所示:
class Counter:
    def __init__(self):
        self.counter = 0
    def increment(self):
        self.counter += 1
    def reset(self):
        self.counter = 0
        接下来,我们添加测试 `Counter` 类的代码如下:
if __name__ == "__main__":
    c = Counter()
    print(f"Counter value: {c.counter}")
    c.increment()
    print(f"Counter value: {c.counter}")
    c.reset()
        你可以在 `ch11/instead_of_global_variable.py` 文件中查看示例的完整代码,运行它应该会给出以下输出:
Counter value: 0
Counter value: 1
        这表明使用类而不是全局变量是有效且可扩展的,因此是推荐的做法。

        性能反模式

        这些反模式会导致效率低下,尤其是在大型应用程序或数据密集型任务中,这会降低性能。我们将关注以下此类反模式:

            +   在循环中不使用 `.join()` 连接字符串

            +   使用全局变量进行缓存

        让我们开始吧。

        在循环中不使用 .join() 连接字符串

        在循环中使用 `+` 或 `+=` 连接字符串会每次创建一个新的字符串对象,这是低效的。最好的解决方案是使用字符串的 `.join()` 方法,该方法专为从序列或可迭代对象中连接字符串时的效率而设计。

        让我们创建一个`concatenate()`函数,其中我们使用`+=`来连接字符串列表中的项,如下所示:
def concatenate(string_list):
    result = ""
    for item in string_list:
        result += item
    return result
        然后,让我们创建一个`better_concatenate()`函数,以实现相同的结果,但使用`str.join()`方法,如下所示:
def better_concatenate(string_list):
    result = "".join(string_list)
    return result
        我们可以使用以下方式测试这两个函数:
if __name__ == "__main__":
    string_list = ["Abc", "Def", "Ghi"]
    print(concatenate(string_list))
    print(better_concatenate(string_list))
        运行代码(在`ch11/concatenate_strings_in_loop.py`文件中)会得到以下输出:
AbcDefGhi
.join() is the recommended practice for performance reasons.
			Using global variables for caching
			Using global variables for caching can seem like a quick and easy solution but often leads to poor maintainability, potential data consistency issues, and difficulties in managing the cache life cycle effectively. A more robust approach involves using specialized caching libraries designed to handle these aspects more efficiently.
			In this example (in the `ch11/caching/using_global_var.py` file), a global dictionary is used to cache results from a function that simulates a time-consuming operation (for example, a database query) done in the `perform_expensive_operation()` function. The complete code for this demonstration is as follows:

import time

import random

全局变量作为缓存

_cache = {}

def get_data(query):

if query in _cache:

return _cache[query]

else:

result = perform_expensive_operation(query)

_cache[query] = result

return result

def perform_expensive_operation(user_id):

time.sleep(random.uniform(0.5, 2.0))

user_data = {

1: {"name": "Alice", "email": "alice@example.com"},

2: {"name": "Bob", "email": "bob@example.com"},

3: {"name": "Charlie", "email": "charlie@example.com"},

}

result = user_data.get(user_id, {"error": "User not found"})

return result

if name == "main":

print(get_data(1))

print(get_data(1))


			Testing the code by running the `python ch11/caching/using_global_var.py` command gives the following output:

{'name': 'Alice', 'email': 'alice@example.com'}

functools.lru_cache() 函数。lru_cache 装饰器提供的 lru_cache 是针对性能优化的,使用高效的数据结构和算法来管理缓存。

        这就是如何使用`functools.lru_cache`实现从耗时函数缓存结果的功能。完整的代码(在`ch11/caching/using_lru_cache.py`文件中)如下所示:
import random
import time
from functools import lru_cache
@lru_cache(maxsize=100)
def get_data(user_id):
    return perform_expensive_operation(user_id)
def perform_expensive_operation(user_id):
    time.sleep(random.uniform(0.5, 2.0))
    user_data = {
        1: {"name": "Alice", "email": "alice@example.com"},
        2: {"name": "Bob", "email": "bob@example.com"},
        3: {"name": "Charlie", "email": "charlie@example.com"},
    }
    result = user_data.get(user_id, {"error": "User not found"})
    return result
if __name__ == "__main__":
    print(get_data(1))
    print(get_data(1))
    print(get_data(2))
    print(get_data(99))
        要测试此代码,请运行`python ch11/caching/using_lru_cache.py`命令。你应该得到以下输出:
{'name': 'Alice', 'email': 'alice@example.com'}
{'name': 'Alice', 'email': 'alice@example.com'}
{'name': 'Bob', 'email': 'bob@example.com'}
{'error': 'User not found'}
        如我们所见,这种方法不仅增强了缓存机制的鲁棒性,还提高了代码的可读性和可维护性。

        摘要

        理解和避免常见的 Python 反模式将帮助你编写更干净、更高效、更易于维护的代码。

        首先,我们介绍了常见的 Python 代码风格违规。然后,我们讨论了几种与正确性相关的反模式,这些反模式可能导致错误。接下来,我们介绍了除了代码风格本身之外,对代码可读性和可维护性不利的实践。最后,我们看到了一些应该避免的反模式,以编写具有良好性能的代码。

        总是记住——最好的代码不仅仅是让它工作,还要让它工作得很好。更进一步,理想情况下,它应该易于维护。

        我们终于到达了这本书的结尾。这是一段旅程。我们从主要设计原则开始,然后转向介绍最流行的设计模式,以及它们如何应用于 Python,最后简要介绍了 Python 的反模式。这有很多!我们讨论的思想和例子帮助我们思考不同的实现选项或技术,以便在遇到用例时选择。无论你选择哪种解决方案,都要记住 Python 倾向于简单性,尽量使用被认为是 Pythonic 的模式和技巧,并避免 Python 的反模式。


posted @ 2025-09-23 21:57  绝不原创的飞龙  阅读(47)  评论(0)    收藏  举报