精通 Python 设计模式——创建型设计模式

设计模式是可复用的编程解决方案,已在诸多真实场景中反复使用并被证明能产生预期效果。它们在程序员之间共享,并随着时间不断改进。该主题之所以广受欢迎,离不开 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著的《Design Patterns: Elements of Reusable Object-Oriented Software》一书。

以下是"四人帮"(Gang of Four)对设计模式的描述引语:

设计模式以系统化的方式为一个通用设计命名、赋予动机并加以解释,该通用设计用来解决面向对象系统中反复出现的设计问题。它描述了问题、解决方案、何时应用该解决方案以及由此带来的影响;同时给出实现提示与示例。解决方案是一种对象与类的通用组织方式,用以解决问题;在具体语境中对该方案进行定制与实现,以解决特定问题。

在面向对象编程(OOP)中,依据所要解决的问题类型与所构建的解决方案类型,设计模式被分为若干类别。在书中,四人帮提出了 23 种设计模式,分属三大类:创建型结构型行为型

本章首先讨论创建型设计模式。它们处理对象创建的不同侧面,目标是在直接创建对象(在 Python 中通常发生在 __init__() 中)不便时,提供更好的替代方案。

本章将涵盖以下主题:

  • 工厂模式(Factory)
  • 生成器/建造者模式(Builder)
  • 原型模式(Prototype)
  • 单例模式(Singleton)
  • 对象池模式(Object Pool)

读完本章,你将牢固理解创建型设计模式、它们在 Python 中是否合适,以及在合适时如何使用。

技术要求

参见第 1 章所述要求。

工厂模式(The factory pattern)

我们从四人帮书中的第一个创建型模式------工厂模式 ------开始。在工厂模式中,客户端(即调用方代码)请求一个对象,但并不知道该对象来自哪里(也就是由哪个类生成)。工厂背后的思想是简化对象创建过程:若通过一个中心函数来创建对象,比起让调用方在各处直接实例化类,更容易追踪哪些对象被创建。工厂通过将"创建对象的代码"与"使用对象的代码"解耦,降低了应用的维护复杂度。

工厂通常有两种形式------工厂方法(factory method) :基于单个方法/函数,根据输入参数返回不同对象;以及抽象工厂(abstract factory) :一组相关联的工厂方法,用于创建同一族的对象。

下面从工厂方法开始讨论这两种形式。

工厂方法(The factory method)

工厂方法基于一个用于处理对象创建任务的单一函数。我们调用它并传入参数说明所需对象的信息,函数据此创建并返回目标对象。

有意思的是,使用工厂方法时,我们无需了解结果对象的实现细节或其来源。

现实类比

现实中,工厂方法可类比为塑料玩具的生产:原料相同,但通过不同模具即可产出不同玩具(不同形状/形象)。这就像一个工厂方法:输入是想要的玩具名称(比如小鸭或小汽车),输出(经过注塑)就是相应塑料玩具。

软件中的例子

在软件世界里,Django Web 框架的表单字段创建使用了工厂方法模式。Django 的 forms 模块(github.com/django/djan...)支持创建多种字段(如 CharFieldEmailField 等),并可通过属性(如 max_lengthrequired)定制部分行为。

何时使用工厂方法(Use cases for the factory method pattern)

如果你发现应用创建对象的代码散落在各处 、难以统一追踪,就应考虑工厂方法。工厂方法将对象创建集中化,使跟踪更容易。实际项目中通常会有多个工厂方法:每个方法负责一类相似对象的创建。比如,一个工厂方法负责连接不同数据库(MySQL、SQLite);另一个负责创建几何对象(圆、三角形);等等。

当你想将对象创建对象使用解耦时,工厂方法也很有用。我们创建对象时并不绑定到某个具体类,只是通过调用函数提供部分需求信息。这样一来,修改该函数很容易,而且无需改动使用它的代码。

另一个值得一提的用例是提升性能与内存使用 :工厂方法可以只在必要时才创建新对象。直接实例化类时,每次都会分配新的内存(除非类内部做了缓存,而这通常并非默认行为)。下面这段代码(ch03/factory/id.py)演示同一类的两个实例拥有不同内存地址:

css 复制代码
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)输出如下:

python 复制代码
False
4330224656
4331646704

说明(NOTE)

你运行时 id() 打印的地址不会与我相同,因为它取决于当前内存布局与分配。但结果应一致:两个地址不同。唯一的例外是你在 REPL(交互式提示符)里输入并执行这段代码时,可能遇到 REPL 的特殊优化,这不是常态。

实现工厂方法模式(Implementing the factory method pattern)

数据有多种形式。存储/检索数据的文件大体分为两类:人类可读二进制 。人类可读格式如 XML、RSS/Atom、YAML、JSON;二进制如 SQLite 的 .sq3、音频 .mp3 等。

本例聚焦两种常见的人类可读格式------XMLJSON。尽管人类可读格式解析通常更慢,但更便于数据交换、检查与修改。因此,除非受限于性能或专有二进制格式,推荐优先使用人类可读格式。

我们有两份输入数据:一个 XML 文件与一个 JSON 文件,目标是解析并提取信息。同时,我们希望集中客户端对这些(以及未来新增)外部服务的访问。我们将用工厂方法来解决。示例仅覆盖 XML 与 JSON,但扩展到更多服务应当很直接。

先看数据文件。

JSON 文件 movies.json:选取了美国电影数据集的一个样例(title、year、director、genre 等):

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:包含人员信息(firstName、lastName、gender 等),结构如下:

容器起始标签:

xml 复制代码
<persons>

一个人员元素:

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>

另一位人员:

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>

第三位人员:

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>

容器结束标签:

bash 复制代码
</persons>

我们将使用 Python 标准库中的 jsonxml.etree.ElementTree 处理 JSON 与 XML。

先导入所需模块(jsonElementTreepathlib),并定义 JSONDataExtractor 类,从文件加载数据,通过 parsed_data 属性获取:

python 复制代码
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 返回结果:

ruby 复制代码
class XMLDataExtractor:
    def __init__(self, filepath: Path):
        self.tree = ET.parse(filepath)

    @property
    def parsed_data(self):
        return self.tree

提供一个工厂函数,依据目标文件扩展名选择合适的数据提取器(不支持则抛异常):

python 复制代码
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 场景:

ini 复制代码
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 的人员,并输出姓名与电话号码:

ini 复制代码
    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}")

最后添加测试代码:

bash 复制代码
if __name__ == "__main__":
    print("* JSON case *")
    extract(case="json")
    print("* XML case *")
    extract(case="xml")

实现概要(ch03/factory/factory_method.py):

  • 定义 JSONDataExtractorXMLDataExtractor 两个数据提取器;
  • 定义工厂函数 extract_factory() 选择合适的提取器;
  • 定义包装与主函数 extract()
  • 在测试代码中分别从 JSON 与 XML 文件抽取并解析文本。

运行示例:

bash 复制代码
python ch03/factory/factory_method.py

预期输出:

markdown 复制代码
* 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 文件或自己创建一个试试。

是否应该使用工厂方法模式?(Should you use the factory method pattern?)

资深 Python 开发者经常对工厂方法模式提出的主要质疑是:对许多用例而言过度工程或不必要复杂 。Python 的动态类型与一等函数常常能更简单直接地解决工厂方法要解决的问题。你往往可以用一个简单函数或类方法直接创建对象,而无需专门的工厂类/函数;这让代码更易读、更"Pythonic",也符合"Simple is better than complex"的哲学。

此外,Python 对默认参数关键字参数 等特性支持良好,使构造器的向后兼容扩展更容易,减少了对独立工厂方法的需求。因此,虽然工厂方法在 Java、C++ 等静态类型语言中是成熟做法,但在更灵活的 Python 中常显得冗长笨拙

为了展示在简单场景下无需工厂方法 也能很好解决问题,示例仓库提供了替代实现(ch03/factory/factory_method_not_needed.py)。正如你所见,那里没有工厂。下面这段摘录说明了我们所说的"在 Python 中,你只需在需要的地方直接创建对象,无需中间函数或类",让代码更加 Pythonic:

ini 复制代码
if case == "json":
    path = dir_path / Path("movies.json")
    data = JSONDataExtractor(path).parsed_data

抽象工厂模式(The abstract factory pattern)

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

下面我们讨论一些示例、适用场景以及一种可行的实现方式。

现实中的例子

在汽车制造中会用到抽象工厂:同一套机械可用于冲压不同车型的部件(车门、侧板、引擎盖、翼子板、后视镜)。由机械组装的车型是可配置的,且可随时切换。

在软件领域,factory_boy 包(github.com/FactoryBoy/...)提供了用于在测试中创建 Django 模型的抽象工厂实现。替代工具有 model_bakerygithub.com/model-baker...)。这两个包都用于创建支持测试特定属性的模型实例。这样做能提升测试的可读性,并避免共享不必要的样板代码。

说明

Django 模型是该框架用于帮助在数据库(表)中存储并交互数据的特殊类。详见 Django 文档:docs.djangoproject.com

适用场景

由于抽象工厂是工厂方法的泛化,它带来的好处与后者一致:

  • 更易跟踪对象创建;
  • 将"对象创建"与"对象使用"解耦;
  • 在某些情形下有潜力改进应用的内存与性能表现。

抽象工厂模式的实现

为演示抽象工厂模式,这里复用 Bruce Eckel 的《Python 3 Patterns, Recipes and Idioms》中的一个示例。设想我们要做一个游戏,或者在应用中嵌入一个迷你游戏来娱乐用户。我们希望至少包含两个游戏:一个给儿童,一个给成人。根据用户输入在运行时决定创建并启动哪个游戏。抽象工厂负责游戏创建部分。

先看儿童向的游戏 FrogWorld 。主角是一只爱吃虫子的青蛙。每个英雄都需要一个好名字,这里由用户在运行时给定。interact_with() 方法描述青蛙与障碍(如虫子、谜题、其他青蛙)的交互:

python 复制代码
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)

障碍物可能很多,但在本例中只有"虫子"。遇到虫子时只有一个动作:吃掉它。

ruby 复制代码
class Bug:
    def __str__(self):
        return "a bug"
    def action(self):
        return "eats it"

FrogWorld 类即为抽象工厂。它的主要职责是创建游戏中的主角与障碍物。将创建方法分开、并保持通用命名(如 make_character()make_obstacle()),可使我们在无需改动代码的情况下,动态切换激活的工厂(也就是激活的游戏):

ruby 复制代码
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 类似,只是主角是法师,敌人是怪物(如兽人),不是吃虫子而是战斗。

法师类定义(与 Frog 类似):

python 复制代码
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)

兽人类定义:

ruby 复制代码
class Ork:
    def __str__(self):
        return "an evil ork"
    def action(self):
        return "kills it"

WizardWorld 类(与 FrogWorld 对应;其障碍为 Ork 实例):

ruby 复制代码
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() 方法触发主角与障碍之间的交互:

ruby 复制代码
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, age);若合法,返回 (True, age),此时我们才关心第二个元素(用户输入的年龄):

python 复制代码
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() 定义及调用。它询问用户名与年龄,并据此决定玩哪个游戏:

ini 复制代码
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):

  • 定义 Frog 与 Bug(FrogWorld 使用);

  • 定义 FrogWorld 抽象工厂;

  • 定义 Wizard 与 Ork(WizardWorld 使用);

  • 定义 WizardWorld 抽象工厂;

  • 定义 GameEnvironment;

  • 定义 validate_age()

  • 定义并调用 main()

    • 获取用户名与年龄输入;
    • 基于年龄选择游戏类;
    • 实例化相应游戏类与 GameEnvironment
    • 调用环境对象的 .play() 开始游戏。

运行程序:

bash 复制代码
python ch03/factory/abstract_factory.py

青少年示例输出:

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

成年示例输出:

diff 复制代码
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!

可以尝试扩展游戏使其更完整:添加更多障碍、更多敌人,随你发挥。

建造者模式(The builder pattern)

前文介绍了两个创建型模式:工厂方法抽象工厂,它们都为在非trivial场景下改进对象创建方式提供了思路。

现在设想这样一种需求:我们要创建的对象由多个部件组成,且必须按步骤 逐步构建;只有全部部件都完成后,对象才算"成品"。这正是建造者模式(builder pattern)发挥作用的地方。建造者模式将复杂对象的构建过程 与其表示 分离;通过将"构建"与"表示"解耦,同样的构建过程可以产出多种不同表示的对象。

现实中的例子

在日常生活中,建造者模式常见于快餐店。无论有多少种汉堡(经典款、芝士汉堡等)和包装(小号盒、中号盒等),制作汉堡与打包 遵循的流程基本一致。经典款与芝士汉堡的区别在于成品表示 ,而不是制作流程。在这个类比中,"指挥者(director)"是收银员------他向后厨下达需要准备什么;"建造者(builder)"则是具体按单制作的后厨同事。

在软件领域,可以参考第三方 Django 库 django-query-buildergithub.com/ambitioninc...)。它依赖建造者模式,用以动态构建SQL 查询,让你可控地组合查询的各个方面,生成从简单到非常复杂的各种查询

与工厂模式的对比

此时两者的区别也许还不够清晰。主要差异在于:

  • 工厂模式 通常一步返回对象;
  • 建造者模式 则是多步构建,且几乎总会有一个**指挥者(director)**参与流程编排。

另一个差异是:工厂模式会立刻 返回创建好的对象;而建造者模式中,客户端 需要在需要时 明确向指挥者索要最终成品。

适用场景

当一个对象存在众多可配置项 、构造器(构造方法)出现多重重载并导致代码易混淆、易出错时,建造者模式尤其有用。

当对象的创建流程并非只是设置初始值,而是包含多个步骤(如参数校验、数据结构搭建、甚至调用外部服务)时,建造者模式也很适合用来封装构建复杂度

建造者模式的实现

下面用"点披萨 "来演示。披萨的制作必须按顺序进行:先和/擀面加酱 ,有了酱才能加配料 ;酱与配料都就位后才能进烤箱。此外,不同披萨的烘烤时长也会因面饼厚度与配料而不同。

首先导入所需模块,并声明若干 Enum 与一个常量。常量 STEP_DELAY 用于在各步骤之间加入延时(秒):

ini 复制代码
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 类而不是某个建造者中的两个原因:

1)强调成品类通常很薄 ,但并不意味着它不能 承担任何职责;

2)通过组合促进代码复用。

定义 Pizza

python 复制代码
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):

ruby 复制代码
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(节选):

ruby 复制代码
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 还提供 pizza 属性,用于将成品返回给调用方:

ruby 复制代码
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。返回一个二元组,首元素表示是否合法:

python 复制代码
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():实例化某个披萨建造者,由指挥者 Waiter 来按流程准备披萨;成品可以在任何需要的时刻"取用/交付":

ini 复制代码
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 完整代码):

  • 引入标准库 Enumtime

  • 声明若干常量:PizzaProgressPizzaDoughPizzaSaucePizzaToppingSTEP_DELAY

  • 定义 Pizza 成品类;

  • 定义两个建造者:MargaritaBuilderCreamyBaconBuilder

  • 定义指挥者 Waiter

  • 定义 validate_style() 以更好地处理异常与输入校验;

  • 定义并调用 main()

    • 校验并基于用户输入选择建造者;
    • 由服务员(指挥者)执行建造流程;
    • 交付构建好的披萨。

运行示例:

bash 复制代码
python ch03/builder.py

示例输出:

vbnet 复制代码
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!

效果不错!

不过......只支持两种披萨未免可惜。想要夏威夷 口味的建造者吗?可以考虑在权衡优劣后使用继承 ;或采用我们在第 1 章"基础设计原则"中提到的组合------它同样有自己的优势。

原型模式(The prototype pattern)

原型模式 通过复制已有对象 来创建新对象,而不是从零开始构造。当初始化一个对象的成本比复制现有对象更高或更复杂时,该模式尤为有用。本质上,原型模式允许你通过复制既有实例来得到一个新实例,从而避免重新初始化所带来的开销。

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

现实中的例子

枝条扦插复制植物就是原型模式的现实类比:你并不从种子培育,而是从现有植株复制出新的个体。

很多 Python 应用都会用到原型模式,但很少特意称其为"原型",因为对象克隆在 Python 里本就是内置能力。

适用场景

  • 当我们已有一个对象,需要保持其原样不变 ,同时又希望在副本上做部分修改时,原型模式很合适。
  • 另一个常见需求是复制一个由数据库填充 、且引用了其他数据库对象的复杂对象。重新构造这样一个对象通常代价高昂(需要多次数据库查询),这时用原型模式会更便捷。

原型模式的实现

如今,即便规模不大的组织也可能通过自家基础设施/DevOps 团队、托管商或云服务商(CSP)维护大量网站与应用。

当你需要管理多个站点时,跟踪信息会变得困难:涉及的 IP 地址、域名及其过期时间、DNS 参数等都要快速 获得。因此,需要某种清单工具

下面设想这些团队在日常如何处理这类数据,并实现一段用于整合与维护数据的程序(而不是把所有信息堆在 Excel 里)。

首先导入标准库 copy

go 复制代码
import copy

系统的核心是一个 Website 类,用来保存名称、域名、描述、站点作者等信息。

__init__() 中,只有部分参数是固定的:namedomaindescription。为了灵活性,客户端还可以通过关键字参数 传入更多属性(name=value),这些键值对会落到 kwargs 字典中。

补充信息

有一个 Python 惯用法:用内置函数 setattr(obj, attr, val) 给对象 obj 设置名为 attr 的属性,值为 val

定义 Website 并在初始化时使用 setattr 处理可选属性:

python 复制代码
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())也包含进去:

python 复制代码
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)

补充信息
vars() 返回对象的 __dict__ 属性,即一个包含对象属性(数据属性与方法)的字典。这对调试很有用,可用于查看对象属性或函数局部变量。但并非所有对象都有 __dict__(如内建类型 list、dict 就没有)。

接着添加实现原型模式的 Prototype 类。其核心是使用 copy.deepcopy()clone() 方法。

说明

使用 copy.deepcopy() 克隆对象时,克隆体的内存地址应与原对象不同

由于克隆意味着可以为可选属性 设定新值,这里使用 attrs 字典配合 setattr 来修改克隆体的属性。为方便起见,Prototype 还提供 register() / unregister() 方法,用一个**注册表(字典)**跟踪可被克隆的对象:

python 复制代码
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(),然后打印结果:

ini 复制代码
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()

ini 复制代码
if __name__ == "__main__":
    main()

实现小结 (见 ch03/prototype.py):

  • 导入 copy 模块;

  • 定义 Website 类,包含 __init__()__str__()

  • 定义上文所示的 Prototype 类;

  • main() 中:

    • 定义 keywords
    • 创建 Website 实例 site1(使用 keywords);
    • 创建 Prototype 并通过 register() 以标识符注册 site1(便于在字典中跟踪);
    • 克隆 site1 得到 site2
    • 打印两个 Website 对象。

在我的机器上运行 python ch03/prototype.py 的示例输出如下:

sql 复制代码
- 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 对象与其克隆体 的信息;并且从每个对象的 ID 值可知,两者的内存地址确实不同

单例模式(The singleton pattern)

作为 OOP 中最早的一批设计模式之一,单例模式 限制某个类只能实例化一个对象 ------当你需要由一个对象来协调系统中的各项操作时,这很有用。

其基本思想是:程序只创建某个类的唯一实例来完成任务。为确保这一点,需要阻止该类被多次实例化,并且还要避免被克隆。

在 Python 社区里,单例 常被视为一种反模式。先看看模式本身,随后再讨论在 Python 中更被推崇的替代做法。

现实中的例子

  • 船长(或舰长):在船上拥有最终决策权,许多请求会因其职责而汇集到他/她处。
  • 办公室里的打印后台(spooler) :通过单一入口协调打印任务,避免冲突并保证有序打印。

适用场景

当你只需要创建一个对象 ,或需要一个对象来维护程序的全局状态时,单例模式就派上用场。其他可能的用例包括:

  • 控制并发访问共享资源:例如管理数据库连接的类。
  • 跨越应用各处、可被多方访问的服务/资源:如日志系统的核心类或某些通用工具。

实现单例模式

如前所述,单例保证一个类只有一个实例 ,同时提供全局访问点 。下面我们实现一个 URLFetcher 来抓取网页内容,希望它在程序中只有一个实例以统一追踪已抓取的 URL。

设想程序的不同部分里都有抓取器,但你想集中地记录所有被抓取过的 URL------这就是单例的典型场景。让程序各处都使用同一个抓取器实例,就能在一个地方维护完整的 URL 列表。

先写一个朴素版本URLFetcher:它的 fetch() 方法抓取页面并把 URL 存入列表:

python 复制代码
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 比较两个实例;若相同则说明是单例:

csharp 复制代码
if __name__ == "__main__":
    print(URLFetcher() is URLFetcher())

运行 ch03/singleton/before_singleton.py,输出为:

python 复制代码
False

这说明当前版本不是单例。要把它改为单例,我们将使用**元类(metaclass)**技术。

补充信息

在 Python 中,元类 是"类的类",它定义了类的构造与行为

创建一个 SingletonType 元类,确保 URLFetcher 只有一个实例:

ini 复制代码
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 使用该元类:

python 复制代码
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() 并调用以测试单例:

css 复制代码
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):

  • 导入所需模块(urllib.request);
  • 定义带有特殊 __call__()SingletonType 元类;
  • 定义 URLFetcher 抓取器类及其 fetch()
  • 添加 main() 并按惯例调用。

运行:

bash 复制代码
python ch03/singleton/singleton.py

预期输出:

less 复制代码
True
Done URLs: ['http://python.org', 'https://planetpython.org/', 'https://www.djangoproject.com/']

此外,会生成 content.html 文件,包含从这些 URL 抓取到的 HTML 内容。程序如预期工作,展示了单例模式的一种用法。

该不该用单例?

虽说单例有其价值,但在 Python 里,它未必是 管理全局状态或资源的最 Pythonic 方式。回看上面的实现,我们会注意到:

  • 使用的技术(如元类)对初学者而言较为晦涩
  • 即便读到 SingletonType 的源码,如果不是名字就暗示"单例",也不易一眼看出其用途;
  • 在 Python 中,开发者往往更偏爱更简单 的替代方案:模块级全局对象

说明

Python 模块 天然就是命名空间,可以包含变量、函数与类,因而非常适合组织与共享全局资源

采用 Brandon Rhodes 在其"全局对象模式(Global Object Pattern) "中介绍的做法(python-patterns.guide/python/modu...),无需复杂的实例化流程或强制类仅有一个实例,就能实现与单例等价的效果。

动手练习 :把本例改写为使用全局对象 的实现。参考代码见 ch03/singleton/instead_of_singleton/example.py(定义全局对象),其用法见 ch03/singleton/instead_of_singleton/use_example.py

对象池模式(The object pool pattern)

对象池模式 是一种创建型设计模式,允许你在需要时复用现有对象 ,而不是每次都新建对象。当新对象的初始化在系统资源、时间等方面代价较高时,此模式尤为有用。

现实中的例子

  • 想象一家汽车租赁 服务。顾客来租车时,服务方不会"制造"一辆新车,而是从可用车辆池中取出一辆。顾客归还后,车辆回到池中,供下一位顾客使用。
  • 另一例子是公共游泳池。并不会每次有人想游泳就重新注满水,而是对池水进行处理后供多人次复用,节省时间与资源。

适用场景

当资源初始化昂贵或耗时 (CPU 周期、内存占用、网络带宽等)时,对象池很有价值。

例如在射击类游戏里,管理子弹 对象:若每次开火都创建一个新子弹,会带来资源开销;取而代之,可以维护一个子弹对象池循环复用。

实现对象池模式

下面为汽车租赁应用实现一个可复用汽车对象池,以避免反复创建和销毁对象。

首先定义 Car 类:

python 复制代码
class Car:
    def __init__(self, make: str, model: str):
        self.make = make
        self.model = model
        self.in_use = False

接着定义 CarPool 的初始化:

ruby 复制代码
class CarPool:
    def __init__(self):
        self._available = []
        self._in_use = []

定义获取汽车的方法:若没有可用车辆,就实例化一辆并加入可用池;否则从可用池取出一辆,标记其状态并加入"使用中"列表:

python 复制代码
    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

定义归还汽车的方法:

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

最后,添加测试代码:

python 复制代码
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

  • 定义 Car 类。
  • 定义 CarPool 类及其 acquire_car()release_car() 方法。
  • 添加简单的测试代码。

运行:

bash 复制代码
python ch03/object_pool.py

预期输出:

yaml 复制代码
Acquire Car 1
Car 1 in use: True
Now release Car 1
Car 1 in use: False

很好!该输出表明我们的对象池实现按预期工作。

总结

本章介绍了创建型设计模式 ,这些模式有助于编写灵活、可维护、模块化 的代码。我们从两种工厂模式开始,它们为对象创建提供了不同优势;随后讨论了建造者模式 ,以更可读、可维护的方式构建复杂对象;接着是原型模式 ,展示如何高效克隆对象 ;最后是单例模式对象池模式,它们都旨在优化资源管理并保证应用中状态的一致性。

掌握了这些用于对象创建的基础模式后,我们已为下一章的结构型设计模式做好准备。

相关推荐
数据智能老司机3 小时前
精通 Python 设计模式——SOLID 原则
python·设计模式·架构
c8i5 小时前
django中的FBV 和 CBV
python·django
c8i5 小时前
python中的闭包和装饰器
python
烛阴6 小时前
【TS 设计模式完全指南】懒加载、缓存与权限控制:代理模式在 TypeScript 中的三大妙用
javascript·设计模式·typescript
bobz9657 小时前
k8s svc 实现的技术演化:iptables --> ipvs --> cilium
架构
云舟吖7 小时前
基于 electron-vite 实现一个 RPA 网页自动化工具
前端·架构
李广坤7 小时前
工厂模式
设计模式
这里有鱼汤8 小时前
小白必看:QMT里的miniQMT入门教程
后端·python
brzhang8 小时前
当AI接管80%的执行,你“不可替代”的价值,藏在这20%里
前端·后端·架构