引言·关于设计模式
设计模式(Design Patterns)是在特定上下文中反复出现的软件设计问题的通用解法。它不是代码模板,而是可复用的设计思想与结构,用于提升系统的可维护性、可扩展性和可理解性。
从工程视角看,设计模式主要应对三类长期问题:
- 变化管理:如何在需求变化时,避免牵一发而动全身。
- 复杂度控制:如何拆分职责,避免对象之间形成隐式耦合。
- 协作与沟通:设计模式提供了一套共享的“设计语言”,降低团队沟通成本。
需要强调的是:设计模式不是目标,而是结果。它通常不是被“选中”的,而是在系统演化过程中自然浮现出来的。
设计模式的整体分类(GoF)
在《设计模式:可复用面向对象软件的基础》中,GoF 将 23 种设计模式分为三类:
1. 创建型(Creational)
关注:对象如何被创建
| 模式 | 核心问题 |
|---|---|
| Singleton | 全局唯一实例 |
| Factory Method | 延迟具体类型决定 |
| Abstract Factory | 产品族一致性 |
| Builder | 复杂对象的逐步构建 |
| Prototype | 通过复制创建对象 |
2. 结构型(Structural)
关注:对象如何组合
| 模式 | 核心问题 |
|---|---|
| Adapter | 接口不兼容 |
| Decorator | 功能叠加而不爆炸继承 |
| Proxy | 访问控制 / 延迟加载 |
| Facade | 简化复杂子系统 |
| Composite | 树形结构统一处理 |
| Bridge | 抽象与实现解耦 |
| Flyweight | 减少对象数量 |
3. 行为型(Behavioral)
关注:对象如何协作
| 模式 | 核心问题 |
|---|---|
| Strategy | 算法可替换 |
| Observer | 状态变化通知 |
| Command | 行为对象化 |
| State | 状态驱动行为 |
| Template Method | 流程固定,步骤可变 |
| Chain of Responsibility | 请求传递 |
| Mediator | 多对多解耦 |
| Visitor | 操作与数据结构解耦 |
| Memento | 状态保存与恢复 |
| Interpreter | 规则解释 |
在日常工程中,我们早已在频繁使用这些模式:
- 插件系统 → Strategy / Factory / Observer
- Nonebot 扩展 → Template Method + Hook
- 队列系统 → Command / Chain of Responsibility
- 模型加载器 → Abstract Factory
只是我们通常是在工程层面使用它们,而不是在教科书层面命名它们。
工厂方法(Factory Method)
设计动机
工厂方法要解决的核心问题是:
调用方不应该依赖具体类,只应该依赖“创建接口”。
典型的坏味道如下:
if t == "a": obj = A()elif t == "b": obj = B()问题并不在 if,而在于:
- 调用方知道了所有具体类
- 新增类型时,调用方必须被修改
- 创建逻辑与业务逻辑发生了耦合
在这个例子中,真正会变化的并不是业务流程,而是:
“在某个条件下该创建哪一种对象”。
工厂方法的作用,就是把这个变化轴从业务逻辑中抽离出来。
结构拆解
工厂方法的最小结构由三部分组成:
- 产品抽象(接口 / 基类)
- 具体产品
- 创建方法(由实现者决定返回哪种产品)
Python 示例
产品接口
from abc import ABC, abstractmethod
class Payment(ABC): @abstractmethod def pay(self, amount: float) -> None: pass具体产品
class AliPay(Payment): def pay(self, amount: float) -> None: print(f"AliPay: {amount}")
class WeChatPay(Payment): def pay(self, amount: float) -> None: print(f"WeChatPay: {amount}")工厂方法接口与实现
class PaymentFactory(ABC): @abstractmethod def create_payment(self) -> Payment: pass
class AliPayFactory(PaymentFactory): def create_payment(self) -> Payment: return AliPay()
class WeChatPayFactory(PaymentFactory): def create_payment(self) -> Payment: return WeChatPay()使用方
def checkout(factory: PaymentFactory, amount: float) -> None: payment = factory.create_payment() payment.pay(amount)工程化变体:注册式工厂
在 Python 工程中,更常见的是“注册表 + 工厂方法”的组合:
class PaymentFactory: _registry: dict[str, type[Payment]] = {}
@classmethod def register(cls, name: str, payment_cls: type[Payment]) -> None: cls._registry[name] = payment_cls
@classmethod def create(cls, name: str) -> Payment: return cls._registry[name]()这种写法本质上仍然是工厂方法,只是把“选择逻辑”配置化、数据化了。
什么时候不该使用工厂方法
需要警惕以下情况:
- 只有一个实现,且几乎不会变化
- 创建过程没有任何条件或配置差异
- 你只是为了“消灭 if”而引入抽象
如果创建逻辑本身没有变化点,那么抽象只会制造噪声。
如果你只记住一件事: 工厂方法的价值不在于创建对象,而在于隔离变化。
从工厂方法到抽象工厂:什么时候升级
当你开始遇到以下情况时,工厂方法往往已经不够用了:
- 一个工厂开始负责创建多个对象
- 这些对象之间存在必须保持一致的组合关系
- 你开始在调用方手动保证“不要混用”
此时,问题已经从“创建哪一个对象”升级为:
“选择哪一整套实现”。
这正是抽象工厂的适用场景。
抽象工厂(Abstract Factory)
设计动机
抽象工厂要解决的核心问题是:
在不暴露具体类的前提下,创建一整套彼此兼容的对象。
关键词不在“抽象”,而在于:产品族一致性。
工厂方法 vs 抽象工厂
| 维度 | 工厂方法 | 抽象工厂 |
|---|---|---|
| 创建对象数量 | 一个 | 一组 |
| 关注点 | 单个产品 | 产品族 |
| 扩展方向 | 新产品 | 新系列 |
| 变化方式 | 增加实现 | 切换整套实现 |
判断标准只有一句话:
如果你关心的是“这些对象是否必须来自同一套实现”, 那你需要的通常是抽象工厂。
Python 示例
产品接口(多个)
class ChatModel(ABC): @abstractmethod def chat(self, text: str) -> str: pass
class EmbeddingModel(ABC): @abstractmethod def embed(self, text: str) -> list[float]: pass产品族实现
class OpenAIChat(ChatModel): def chat(self, text: str) -> str: return f"OpenAI chat: {text}"
class OpenAIEmbedding(EmbeddingModel): def embed(self, text: str) -> list[float]: return [0.1, 0.2]class LocalChat(ChatModel): def chat(self, text: str) -> str: return f"Local chat: {text}"
class LocalEmbedding(EmbeddingModel): def embed(self, text: str) -> list[float]: return [0.9, 0.8]抽象工厂接口与实现
class ModelFactory(ABC): @abstractmethod def create_chat(self) -> ChatModel: pass
@abstractmethod def create_embedding(self) -> EmbeddingModel: passclass OpenAIFactory(ModelFactory): def create_chat(self) -> ChatModel: return OpenAIChat()
def create_embedding(self) -> EmbeddingModel: return OpenAIEmbedding()抽象工厂的工程价值
- 组合合法性由结构保证,而不是由人为约定
- 系列切换是原子操作,只需替换工厂实例
factory = OpenAIFactory() # 一行切换整套实现这在模型后端切换、运行环境差异、能力分级等场景中尤为常见。
什么时候不该使用抽象工厂
- 产品之间不存在强一致性要求
- 交叉组合本身是合理且必要的
- 产品族边界尚不稳定
过早引入抽象工厂,往往意味着过早冻结系统结构。
如果你只记住一件事: 抽象工厂的价值不在于“创建很多对象”,而在于“约束组合方式”。