四人组的咖啡馆:设计模式的诞生与永恒追问 / The Café of Four: The Birth and Eternal Quest of Design Patterns
第 1 章:四人组的咖啡馆:设计模式的诞生与永恒追问
The Café of Four: The Birth and Eternal Quest of Design Patterns
---
1994年12月的一个早晨,Urbana小镇Cygnet咖啡馆靠窗最里面的那张桌子,从早上九点就被四个男人占住了。窗外密歇根湖吹来的雪已经下了三天,窗内是四杯续了又续的黑咖啡、九张画满草图的餐巾纸,以及一场已经持续了十八个月的争论。Erich Gamma 用铅笔在餐巾纸边缘敲着节拍,Richard Helm 正在 ThinkPad 上敲出"Decorator 模式"小节的最终修订版,Ralph Johnson 推门进来时围巾上还挂着冰碴,John Vlissides 则把刚改完的"Visitor 模式"塞进了寄给 Addison-Wesley 编辑的第三个信封——这本书距离最终出版只剩最后三章。这本日后被全球程序员简称为 *GoF* 的《Design Patterns: Elements of Reusable Object-Oriented Software》,就诞生在这样寒冷的早晨、这样喧闹的咖啡桌旁。
本书要回答的根本问题是:设计模式(Design Patterns)从何而来?它为什么成为面向对象开发者的必修课?我们又该如何真正"遇见"这 23 个模式? 让我们先回到那个早晨,把时间线拉长,从 Christopher Alexander 的建筑学讲起,再走到今天每一个工程师的 IDE 里。
1.1 四人组的画像:散落各地的拼图终于凑齐 / The Portraits of the Four
任何一个学术运动背后都有几个关键人物,*GoF* 也不例外。这四个名字出现的频率太高,以至于许多初学者会以为他们一定同属一个研究机构——但事实恰恰相反:他们来自三块大陆、两个时区、四个不同的软件文化圈,直到 1993 年初才第一次围坐到一起。
Erich Gamma(生于 1961 年,苏黎世)当时是 IBM 瑞士分部的技术顾问,后来成为 JUnit 和 Eclipse 的核心架构师之一。他在 1992 年完成了关于对象行为的博士论文,热衷于把建筑师常用的"模式语言(Pattern Language)"思想移植到软件领域。Gamma 的工程直觉极强,他写的代码片段在组内被当作"参考实现"传阅。
Richard Helm(生于 1963 年,墨尔本)从澳大利亚国立大学毕业后加入 IBM 多伦多实验室,是四人中"最像顾问"的那位。他在 1990 年代初为银行、电信、保险公司做面向对象咨询,发现不同行业的工程师反复犯同样的设计错误——这一观察后来成为 *GoF* 全书的隐藏主线。
Ralph Johnson(生于 1955 年,伊利诺伊州)是四人中唯一的学术派,长期担任 UIUC(伊利诺伊大学厄巴纳-香槟分校)计算机科学教授,研究领域是面向对象数据库和分布式系统。Johnson 是连接学术界与工业界的关键桥梁,他也是把 Hillside Group 这个非正式模式社团变成"模式运动(Pattern Movement)"枢纽的灵魂人物。
John Vlissides(生于 1961 年,纽约)当时在 IBM T.J. Watson 研究中心,是四人中最晚加入的成员,但承担了书稿的整体结构编排和定稿工作。Vlissides 后来写了 *Pattern Hatching* 一书,专门讨论模式在真实工程中的"反模式"和误用——他早逝于 2005 年,被模式社区称为"the pattern prophet"。
把他们凑到一起的不是某个项目,而是 *Hill Side Group* 的邮件列表。这个由 Ward Cunningham 和 Kent Beck 在 1987 年发起的非正式社团,在 1993 年汇集了大约 200 名对"软件中的模式"感兴趣的工程师。1993 年春天,Gamma、Helm、Johnson、Vlissides 四人在加州山景城的一次 PLoP(Pattern Languages of Programs)会议上第一次坐到同一张桌子前,他们用了整整一个下午争论一个问题:为什么 1990 年代的程序员没有一份"前人经验清单"? 那个问题最终演化成 23 个模式、477 页正文、24 张图。这就是 *GoF* 真正的起点。
1.2 从亚历山大到 GoF:一场跨越十二年的思想接力 / From Alexander to GoF: A Twelve-Year Relay of Ideas
要把 *GoF* 真正读懂,必须先回到一位建筑师。
Christopher Alexander 是英国裔美国建筑学家,加州大学伯克利分校的终身教授。他在 1977 年出版了一本改变了整个设计学科的书——*A Pattern Language: Towns, Buildings, Construction*(牛津大学出版社)。Alexander 用 253 个"模式"描述了从"独立住宅的窗户位置"到"城市街区的邻里氛围"的完整设计语言。每个模式都遵循一个固定结构:背景(Context)→ 问题(Problem)→ 解决方案(Solution)→ 后果(Consequences)。
例如 Alexander 的第 36 号模式 *Degree of Publicness*(公共性程度):背景是任何一群住宅;问题是"如何让住宅既私密又能在需要时亲近公共空间";解决方案是"按从最公共到最私密的顺序排列房间,公共房间靠街道一侧,私密房间靠庭院一侧";后果是"人在家中能在三层空间之间自由切换社交距离"。读 Alexander 的时候你会有一种奇特的体验——他讲的虽然是窗户、墙、庭院,但这些逻辑几乎可以一字不改地搬到软件里。
1987 年,OOPSLA(面向对象编程、系统、语言与应用会议)在美国佛罗里达州的奥兰多举行。Ward Cunningham 和 Kent Beck(注意,这两位后来都因为极限编程和测试驱动开发而闻名)在这次会议上发表了第一篇把 Alexander 的模式思想应用到软件设计的论文——*Using Pattern Languages for Object-Oriented Programs*。这篇论文不长,但它做了一件关键的事:把 Alexander 的"模式语言"翻译成了 Smalltalk 程序员能听懂的语言。他们讨论的 5 个模式——Window、Panel、Model-View-Controller、Editor-View-Controller、Constraint——后来成为 *GoF* 书中 Observer、Composite、Strategy 等模式的雏形。
1991 年,Bruce Anderson 和 Bruce Whitenack 在 ECOOP(欧洲面向对象编程会议)上发表了 *Object-Oriented Design Patterns* 一文,系统梳理了 Smalltalk 编程中常见的 7 个模式;同年,Hillside Group 在伊利诺伊举行了第一届 PLoP 大会。从 1991 到 1994 年,全球大约 200 位研究者贡献了 60 多个候选模式,*GoF* 四人组从中筛选、归并、精炼,最终交付了 23 个最稳定、最通用、最具教学价值的模式。
这条线索其实揭示了一件常被忽视的事:*GoF* 不是"创造"了模式,而是发现并命名了早已在程序员群体中反复出现的最佳实践。模式从来都在那里,*GoF* 给了它们一张地图。
1.3 六大原则:模式的 DNA 双螺旋 / Six Principles: The DNA of Patterns
在深入 23 个模式之前,必须先建立它们的"母语"——一组关于"什么是好的面向对象设计"的元规则。*GoF* 书中没有显式列出这套规则,但每一个模式的"意图(Intent)"段落都能拆解出这些原则的影子。它们是 SOLID 五原则 加上本书特别强调的第六原则——封装变化(Encapsulate What Varies)。
我们用最简洁的方式逐条展开,并给出最具代表性的对应模式:
S — Single Responsibility Principle(单一职责原则) 一个类应该只有一个引起它变化的原因。Decorator 模式(书中的第 4 章)是这条原则的典范:它把"动态添加职责"这个变化点封装到装饰器类中,而原始对象的职责保持单一。如果一个类同时承担"计算工资"和"持久化到数据库"两件事,它就违反 SRP。
O — Open/Closed Principle(开放/封闭原则) 软件实体应该对扩展开放、对修改封闭。这是 *GoF* 全书的灵魂。Strategy 模式(本书第 11 章)是 OCP 的教科书案例——见下面的代码对比:
```python
反例:违反 OCP,每次新增国家都要修改此类
class TaxCalculator: def calculate(self, country, amount): if country == "US": return amount * 0.07 elif country == "CN": return amount * 0.13 elif country == "DE": return amount * 0.19
想加日本?打开这个文件,改 if-elif
正例:遵循 OCP,新增国家只需新增一个类
from abc import ABC, abstractmethod
class TaxStrategy(ABC): @abstractmethod def calculate(self, amount: float) -> float: ...
class USTax(TaxStrategy): def calculate(self, amount): return amount * 0.07
class CNTax(TaxStrategy): def calculate(self, amount): return amount * 0.13
class DETax(TaxStrategy): def calculate(self, amount): return amount * 0.19
class TaxCalculator: def __init__(self, strategy: TaxStrategy): self._strategy = strategy def compute(self, amount): return self._strategy.calculate(amount)
想加日本?写一个 JapanTax 类,其余代码一行不动
```
从反例到正例的迁移,本质是把"if-else 分支"这条变化的轴封装进了 `TaxStrategy` 这个抽象里。`TaxCalculator` 关闭了对修改的开放,只对扩展保持开放。
L — Liskov Substitution Principle(里氏替换原则) 子类必须能够替换父类而不破坏程序正确性。Template Method(本书第 12 章)是最朴素的体现:父类定义算法骨架,子类在不改变骨架的前提下替换某些步骤。*GoF* 第 261 页用一句话点破:"继承的目的在于复用,而非破坏。"
I — Interface Segregation Principle(接口隔离原则) 客户端不应被迫依赖它不使用的接口。Facade 模式(本书第 5 章)和 Adapter 模式(本书第 2 章)共同体现了这条原则:它们都在做"把臃肿的接口切薄"这件事。
D — Dependency Inversion Principle(依赖倒置原则) 高层模块不应依赖低层模块,两者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。Abstract Factory(本书第 4 章)和 Dependency Injection(虽然不在 23 模式之内,但与 Abstract Factory 同源)是这条原则的工业级实现。
第六原则 — Encapsulate What Varies(封装变化)
这条不在 SOLID 缩写里,但它比任何 SOLID 原则都更"古老"。它可以追溯到 1994 年 John Vlissides 的一次内部分享:*"Design patterns are basically about encapsulating variation."* 如果你打开任何一个 *GoF* 模式的"意图"段落,你会发现它们本质上都在做同一件事——找到系统中会变化的部分,把它单独抽出来,让不变的部分依赖它。Strategy 封装了"算法变化",Decorator 封装了"职责变化",Observer 封装了"状态变化的通知",State 封装了"对象状态的变化"。
一个简单的判定清单:当你读完一个模式,自问三个问题—— 1. 这个模式封装了什么变化?(What varies?) 2. 这个变化被封装到了哪个抽象里?(Into what abstraction?) 3. 哪些代码现在可以独立演化?(What can now evolve independently?)
这三个问题答得好,你就真正读懂了那个模式;答不上来,你只是记住了一个 UML 图。
1.4 23 模式的三族图谱:一张全景航海图 / The Three Families of 23 Patterns
*GoF* 把 23 个模式分成三族。这种分类不是凭空发明的,而是基于一个朴素的问题:模式关心的究竟是对象的什么?
- **创建型模式(Creational Patterns,5 个)**——关注**对象创建机制**。它们用一套受控的接口封装"用什么类、怎么创建、何时创建"这些决策,让系统在创建对象这件事上更灵活、更可复用。
- **结构型模式(Structural Patterns,7 个)**——关注**类和对象的组合方式**。它们通过继承、组合、代理等手段,把零散的类组装成更大的结构,同时保持结构的灵活和高效。
- **行为型模式(Behavioral Patterns,11 个)**——关注**对象之间的职责分配和通信**。它们不仅描述类或对象的模式,还描述它们之间的消息流、协作模式。
下面是这张图谱的全貌——
```mermaid flowchart TD Root[GoF 23 Patterns] Root --> Cre[Creational 5] Root --> Str[Structural 7] Root --> Beh[Behavioral 11]
Cre --> Sing[Singleton] Cre --> FM[Factory Method] Cre --> AF[Abstract Factory] Cre --> Bld[Builder] Cre --> Pro[Prototype]
Str --> Adp[Adapter] Str --> Bri[Bridge] Str --> Cmp[Composite] Str --> Dec[Decorator] Str --> Fac[Facade] Str --> Fly[Flyweight] Str --> Prx[Proxy]
Beh --> CoR[Chain of Responsibility] Beh --> Cmd[Command] Beh --> Int[Interpreter] Beh --> Itr[Iterator] Beh --> Med[Mediator] Beh --> Mem[Memento] Beh --> Obs[Observer] Beh --> Sta[State] Beh --> Str2[Strategy] Beh --> TM[Template Method] Beh --> Vis[Visitor] ```
你可以把这张三族图谱当作全书的航海图。三族之间的关系可以这样记忆:创建型关注"事物从何而来",结构型关注"事物如何组合",行为型关注"事物之间如何对话"——这是对象从诞生到协作的完整生命周期。
接下来三章将依次进入三族,每一族都会用一个"代表人物"作为引子,把族内的模式串成一个故事。第 2 章是 Singleton——创建型的第一位出场的角色。它可能是 23 个模式中最简单、也最有争议的一个:它只有十几行代码,却引发了关于"全局状态是否合理"的三十年争论。
1.5 阅读本书的四层叙述法:故事、意图、代码、思想 / The Four-Layer Narrative
一本技术书如果只讲"是什么、怎么做",读者很快会忘;如果只讲"思想、哲学",读者会晕。*GoF* 原书采取了"模式名 + 意图 + 动机 + 适用性 + 结构 + 参与者 + 协作 + 后果 + 实现 + 示例代码 + 已知应用 + 相关模式"的十二段式骨架,堪称模式描述的"金本位"。但那套骨架对初学者过于密集。
本书采用四层叙述来降低门槛:
第一层:故事(Story) 每章开篇用一个具体场景引入模式。这个场景不是抽象的"假设有 A、B、C 类",而是一个有血有肉的工程难题——可能是电商促销规则的爆炸式增长,可能是游戏角色的状态切换,可能是 GUI 框架的窗口嵌套。故事负责把读者"骗"进模式的核心问题。
第二层:意图(Intent) 这是模式最精炼的一句话。*GoF* 原书的"Intent"段落往往只有两三行,但密度极高。比如 Observer 的 Intent 是:"Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically." 本书会保留这种密度,并在意图段落之后用"为何这样设计(Why this design?)"补充 2-3 段解释意图背后的判断依据。
**第三层:代码(Co