设计模式:范式的应用

1994 年,GoF 做了一件重要的事:不是发明解决方案,而是给已经存在的解决方案命名

Observer、Strategy、Factory——这些解法在 GoF 之前就被无数程序员独立发现过。GoF 的贡献不是创造,而是命名

命名本身就是力量。有了”Observer”这个词,你一句话就能对齐整个团队的设计意图;没有这个词,你要画图、解释结构、说明动机——每次都从零开始。模式的第一个价值是词汇,不是代码模板。

但 Peter Norvig 在 1996 年指出:GoF 的 23 个模式中,有 16 个在动态语言里”透明”——不是消失了,而是语言已经把它表达的能力内建了。Strategy 在 Python 里退化为传一个函数,Iterator 在现代语言里退化为 for...of

这引出一个更深的问题:如果模式在更强大的语言里消失,它们究竟是什么?

一、模式是什么

不是模板,是被命名的结构

模式不是要背诵的 23 个条目,不是可以粘贴的代码模板。它是一个被命名的、反复出现的结构性解决方案

三个核心要素:

  • 问题:一个反复出现的设计场景(“需要在不修改算法调用方的前提下替换算法”)
  • 解法:一种结构关系(“把算法抽象成接口,运行时注入具体实现”)
  • 名字:让解法可以被识别和传递(“Strategy 模式”)

三者缺一不可,但名字是核心。没有名字的解法存在于每个人的经验里,无法流通;有了名字,它成为共享词汇,跨团队、跨项目传播,成为工程师之间的暗语。

模式是范式的词汇表

每个范式成熟之后,都沉淀出一套词汇——用来表达这个范式内的常见结构,捕捉这个范式下反复出现的问题和解法:

范式核心词汇
OOPFactory、Singleton、Observer、Strategy、Decorator、Composite
函数式Functor、Monad、Lens、Fold、Free Monad
响应式Observable、Signal、Operator、Backpressure、Subject
过程式状态机、责任链、命令队列、协程
泛型Typeclass、Trait bound、Zero-cost abstraction、GAT

GoF 的 23 个模式是 OOP 的词汇表——它们出现是因为 OOP 的封装和继承在工程实践中产生了反复出现的结构性问题,这些问题被识别、被命名、被系统化。

理解一个范式,很大程度上就是学习它的词汇表。词汇表越丰富,说明这个范式越成熟,处理过的问题越多。

二、命名与压缩:模式的认知价值

命名即压缩

专家之间的沟通为什么高效?因为他们共享词汇。

“这里用 Strategy”,七个字,传达了:把算法抽象成接口、运行时注入、调用方不感知具体实现的完整结构意图。没有这个词,你需要画图、解释、举例。

模式名是有损压缩——丢掉了实现细节,保留了设计意图。压缩率越高,沟通效率越高。这就是为什么 GoF 即使在 30 年后仍然被引用:它提供了一套高效的设计语言,让程序员能在短时间内对齐复杂的结构决策。

代价是误解的风险——用同一个词,不同人可能理解的是不同变体。“你说的 Factory 是 Factory Method 还是 Abstract Factory?“这是词汇共享的暗面:名字压缩了信息,也压缩掉了精确度。

模式密度:语言表达力的反指标

一个反直觉的认知工具:代码库里需要手动实现的模式越多,语言的表达力就越弱。

Java 实现 Strategy 模式:

interface SortStrategy {
    void sort(int[] data);
}
 
class BubbleSort implements SortStrategy {
    public void sort(int[] data) { ... }
}
 
class Sorter {
    private SortStrategy strategy;
    public Sorter(SortStrategy strategy) { this.strategy = strategy; }
    public void sort(int[] data) { strategy.sort(data); }
}

Python 实现同样意图:

def sort_with(data, strategy):
    strategy(data)
 
sort_with(data, bubble_sort)

Strategy 模式没有消失,它表达的意图(“把算法作为参数传入”)仍然存在。消失的是实现它所需的样板代码——因为 Python 的一等函数直接提供了这个能力。

这就是模式密度作为反指标的含义:看一个语言需要多少显式模式,就能判断它在哪些维度上缺乏表达力。Java 高密度,Haskell 低密度,不是因为 Java 程序员不懂设计,而是因为 Haskell 已经把更多的设计意图内建为语言特性。

三、从模式到语言特性的演化弧

模式是语言特性等待被发明的化石

设计模式有一条清晰的演化弧线:

问题反复出现

程序员独立发现解法

解法被命名为模式(GoF 阶段)

模式被广泛采用

下一代语言把它内建为语法或标准库

模式"消失"——能力留下,名字不再需要

这不是猜测,是已经发生的历史:

模式演化轨迹当前状态
IteratorGoF(1994)→ for...of / .iter()所有现代语言已内建,模式消失
SingletonGoF(1994)→ 模块系统import 天然单例,双重检查锁成为历史
StrategyGoF(1994)→ 高阶函数FP 语言中完全透明
DecoratorGoF(1994)→ @decorator 语法Python / TypeScript 已成语言内建
ObserverGoF(1994)→ EventEmitter → SignalSignal 正在成为语言级原语(TC39 提案)
MonadHaskell 理论 → async/await异步 Monad 已被所有现代语言内建

最后一个值得展开:async/await 是 Monad 的工业化落地。

# async/await 的本质:把"可能异步"编码进类型
async def fetch_user(id: int) -> User:
    data = await http.get(f"/users/{id}")
    return User.from_dict(data)

await 就是 Monad 的 bind>>=)——把一个带上下文的值(Coroutine)展开,取出内部值,继续计算。大多数人用 async/await 但不知道它叫 Monad,更不会去看 Haskell 的类型论。这正是”模式内建后变透明”的完美例证:能力被所有人使用,名字只有专家才知道。

消解不是消亡

模式消解后,它解决的问题并没有消失,只是解决方式变得更自然:

// Rust:Iterator 模式完全内建
// 没有 hasNext() / next(),直接用迭代器链
let result: Vec<i32> = numbers.iter()
    .filter(|&&x| x > 0)
    .map(|&x| x * 2)
    .collect();
# Python:Singleton 不需要模式
# 模块在进程内只被加载一次——天然单例
# config.py
DATABASE_URL = "postgresql://..."
 
# main.py
from config import DATABASE_URL  # 每次 import 拿到同一个对象

消解的方向揭示了语言的哲学立场:一门语言选择内建哪些模式,就是在宣布它认为哪些设计意图足够基础、足够普遍,值得成为语言的一部分。

Python 内建了 Iterator 和 Decorator,说明它认为”遍历抽象”和”动态增强”是基础能力。Rust 内建了 trait 和 enum + match,说明它认为”行为约定”和”穷尽类型”是基础能力。

四、GoF 三类与范式的对应

GoF 把 23 个模式分为三类,每类的出现都有其范式根源。

创建型:OOP 封装的代价

创建型模式(Factory、Builder、Singleton、Prototype)解决”对象如何被创建”。

这类模式的出现是因为 OOP 的封装把构造逻辑变复杂了。当一个对象的创建依赖于配置、依赖于运行时条件、依赖于产品族的一致性时,直接调用构造函数不够用——需要专门的词汇来管理创建过程。

# Factory Method:创建与使用解耦
class NotificationFactory:
    @staticmethod
    def create(channel: str) -> Notification:
        match channel:
            case "email": return EmailNotification()
            case "sms":   return SMSNotification()
            case "push":  return PushNotification()

函数式语言几乎不需要创建型模式——因为值是不可变的,“创建”就是调用函数,没有复杂的初始化过程需要管理。

结构型:组合论的工程化

结构型模式(Adapter、Decorator、Proxy、Composite、Bridge)解决”对象如何连接”——直接对应 01 中 OOP 组合论的工程实践。

Decorator 模式是组合优于继承的经典应用:

# 继承:紧耦合,修改父类行为
class LoggingList(list):
    def append(self, item):
        print(f"Adding: {item}")
        super().append(item)
 
# Decorator:组合,动态添加行为
class LoggingDecorator:
    def __init__(self, wrapped):
        self._wrapped = wrapped
 
    def append(self, item):
        print(f"Adding: {item}")
        self._wrapped.append(item)

Adapter 在结构化类型的语言里自然消解:TypeScript 的”形状即身份”让接口转换不需要显式适配器;Go 的隐式接口实现让类型只要有正确方法就自动满足接口。

行为型:时态观的工程投影

行为型模式(Observer、Strategy、State、Command、Chain of Responsibility)解决”对象如何交互”——是 OOP 时态观的工程延伸:当状态变化时,谁负责通知,谁负责响应?

State 模式是最直接的时态工程化:把状态机的每个状态显式建模,状态转移封装在状态对象内部。

# State 模式:显式状态机
class TrafficLight:
    def __init__(self): self.state = RedState()
    def change(self): self.state = self.state.next()
 
class RedState:
    def next(self): return GreenState()
 
class GreenState:
    def next(self): return YellowState()
 
class YellowState:
    def next(self): return RedState()

Rust 把这个模式提升为语言特性:enum + match 自带穷尽检查,编译器保证你没有遗漏任何状态:

enum TrafficLight { Red, Yellow, Green }
 
fn next(light: TrafficLight) -> TrafficLight {
    match light {
        TrafficLight::Red    => TrafficLight::Green,
        TrafficLight::Green  => TrafficLight::Yellow,
        TrafficLight::Yellow => TrafficLight::Red,
    }
    // 漏掉任何一个分支 → 编译错误
}

State 模式在 Rust 里不需要三个类,只需要一个 enum。模式消解了,但类型安全的状态机能力反而更强了。

反模式:词汇表的阴暗面

模式的词汇表有正面(识别好的结构),也有反面:反模式——命名常见的错误结构,让团队能识别并修复它。

反模式它违背的设计原则模式层面的解药
God ClassSRP——单一职责拆分为多个专职对象
Callback Hell可读性与可维护性Promise → async/await(Monad 内建)
Golden Hammer适配性——工具匹配场景理解多种范式,按场景选择
Anemic Domain ModelOOP 封装——行为应在数据旁领域对象承担自己的业务逻辑
Premature Optimization认知成本与实际收益先让代码正确,再让代码快

Callback Hell 是最好的反模式例子——它不只是代码风格问题,而是异步 Monad 缺失时的结构性必然。async/await 出现后,Callback Hell 从”需要靠纪律避免的反模式”变成了”语言不允许你写出来的结构”。反模式的消解和模式的消解同理:语言内建了正确的做法,错误的做法就自然变难写了。

五、多语对照:模式消解地图

三门语言在模式消解上的哲学选择:

模式PythonTypeScriptRust
Strategy直接传函数(Callable函数类型 / 接口impl Fn / trait 对象
Observer回调列表 / asyncioEventEmitter / Signalchannel(broadcast)
Iterator生成器 + __iter__ 协议Symbol.iterator / GeneratorIterator trait(零成本)
Decorator@decorator 语法(语言内建)TC39 Decorators组合 + trait 泛型包装
Singleton模块天然单例const 模块导出static / OnceLock
Adapter鸭子类型自动适配结构化类型自动满足trait 隐式实现
Stateenum(3.10+)+ matchDiscriminated unionenum + 穷尽 match
Command闭包 / 可调用对象闭包 / 函数类型闭包 / Box<dyn Fn>

消解最彻底的方向:

  • Python:行为型模式因一等函数大量消解;模块系统消解了 Singleton;鸭子类型消解了 Adapter
  • TypeScript:结构化类型消解了所有需要显式声明实现关系的模式;类型体操本身成为一套新的声明式词汇
  • Rust:trait + enum + match 重新定义了行为型模式的表达方式;没有继承,模板方法随之消失;所有权系统隐式实现了 RAII(资源获取即初始化)

三门语言的哲学立场总结:

  • Python:信任程序员,把能省的样板全省掉,鸭子类型是终极适配器
  • TypeScript:类型系统本身是更强大的设计词汇,结构即契约
  • Rust:编译器替代运行时检查,让模式的约束由类型系统而非纪律来保证

延伸阅读

  • Gamma et al.《Design Patterns: Elements of Reusable Object-Oriented Software》(1994) — GoF 经典
  • Norvig, Peter. “Design Patterns in Dynamic Languages.” (1996) — 模式消解的先驱论文
  • Seemann, Mark. “Patterns as Signs of Language Deficiency.” — 模式密度作为语言表达力反指标
  • Evans, Eric.《Domain-Driven Design》(2003) — 领域层面的词汇表建设

关联 meta 维度