《设计模式:可复用面向对象软件的基础》读书笔记

GoF(Erich Gamma / Richard Helm / Ralph Johnson / John Vlissides)1995 年经典,俗称”设计模式”或 GoF 书。本书确立了 23 个经典设计模式的目录(catalog),并给出描述模式的标准框架。全文字版 epub 已完整消化,本笔记按”两大设计原则 → 模式描述框架 → 23 模式目录 → 选型与使用 → 案例研究”组织。

一、两大核心设计原则(全书之魂)

这两条原则贯穿全部 23 个模式,比任何单个模式都重要:

  1. 针对接口编程,而不是针对实现编程(Program to an interface, not an implementation)

    • 区分”类”与”类型”:类定义实现(内部状态+操作实现),类型只定义接口(可响应的请求集合)。一个对象可有多个类型,不同类的对象可有同一类型。
    • 区分”类继承”与”接口继承(子类型化)“:类继承是代码/表示的复用机制;接口继承描述”一个对象可替代另一个对象”的时机。
    • 实践含义:变量不要声明为具体类的实例,只承诺抽象类定义的接口。这极大减少子系统间的实现依赖。
    • 创建型模式(Abstract Factory / Builder / Factory Method / Prototype / Singleton)正是为了在实例化时刻把接口与实现透明地关联起来,保证系统”以接口而非实现来书写”。
  2. 优先使用对象组合,而不是类继承(Favor object composition over class inheritance)

    • 类继承 = 白盒复用:父类内部对子类可见;编译期静态定义;“继承破坏封装”——子类实现与父类实现强绑定,父类一改子类就得跟。
    • 对象组合 = 黑盒复用:只通过接口访问,封装不被破坏;运行期动态可替换;类保持小而专注,不会长成不可管理的巨兽。
    • 代价:基于组合的设计对象数量更多(类更少),系统行为散布在对象协作关系中而非定义在单个类里。
    • 折中方案:只从抽象类继承(几乎不提供实现),或委托(Delegation)——组合+继承的结合,把部分职责委托给关联对象,比继承更灵活但可能增加对象数量。

二、模式的描述框架(9 要素)

本书为每个模式建立了标准描述模板,这也是”模式语言”这一表达形式的开创性贡献:

  • Pattern Name and Classification(模式名与分类)
  • Intent(意图)— 一句话说明模式做什么、解决什么问题
  • Also Known As(别名)
  • Motivation(动机)— 问题与解法的示意场景
  • Applicability(适用性)— 何时使用该模式,何时不该用
  • Structure / Participants / Collaborations(结构/参与者/协作)— UML 图示
  • Consequences(效果)— 权衡取舍,评价方案的关键部分
  • Implementation(实现)— 实现难点与技巧
  • Sample Code / Known Uses / Related Patterns(样例代码/已知应用/相关模式)

三、分类体系:目的 × 范围

二维分类(Table 1.1):

  • 目的(purpose):创建型(对象创建过程)/ 结构型(类或对象的组合)/ 行为型(类或对象的交互与职责分配)
  • 范围(scope):类模式(类与子类关系,靠继承,编译期静态)/ 对象模式(对象间关系,运行期动态)。绝大多数模式属于对象范围。

四、23 个模式目录(按意图浓缩)

创建型(Creational,5 个)

模式意图一句话
Abstract Factory提供创建”相关对象家族”的接口,不指定具体类
Builder将复杂对象构造与其表示分离,同一构造过程可产生不同表示
Factory Method定义创建对象的接口,让子类决定实例化哪个类(延迟到子类)
Prototype用原型实例指定待创建对象的种类,通过复制原型创建新对象
Singleton保证一个类只有一个实例,并提供全局访问点

结构型(Structural,7 个)

模式意图一句话
Adapter把一个类的接口转换成客户端期望的另一个接口,让不兼容的类协作
Bridge将抽象与其实现解耦,使二者可独立变化
Composite把对象组合成树形”部分-整体”层次,客户端统一对待单个对象与组合
Decorator动态给对象附加额外职责,是生成子类扩展功能的灵活替代
Facade为子系统的一组接口提供统一的高层接口,使子系统更易用
Flyweight用共享技术高效支持大量细粒度对象
Proxy为另一对象提供替身/占位符以控制访问

行为型(Behavioral,11 个)

模式意图一句话
Chain of Responsibility请求沿接收者链传递,直到有对象处理,解耦发送者与接收者
Command把请求封装为对象,支持参数化、排队、日志与可撤销操作
Interpreter给定语言,定义其文法表示并用解释器解释句子
Iterator顺序访问聚合元素而不暴露其内部表示
Mediator用一个对象封装一组对象的交互方式,促进松散耦合
Memento不破坏封装地捕获并外部化对象内部状态,以便日后恢复
Observer定义一对多依赖,一个对象状态变化时所有依赖者自动获通知并更新
State对象内部状态改变时改变其行为,看起来像改变了类
Strategy定义算法族并各自封装、可互换,算法与使用方独立变化
Template Method在操作里定义算法骨架,把某些步骤延迟到子类
Visitor表示作用于对象结构元素上的操作,可在不改变元素类的前提下定义新操作

五、模式如何解决问题 & 如何选用

设计模式解决的核心反复出现的问题(第 1 章):

  • 找到合适的对象:靠领域词汇分析(名词→候选对象)与职责 CRC 法。
  • 决定对象粒度:粗粒度系统更小更快但难定制;模式覆盖从 Strategy(细)到框架/ MVC(粗)的粒度谱。
  • 指定对象接口与指定对象实现:接口与实现分离后,可为不同场景换实现而不动客户代码。
  • 运行期与编译期结构:继承是编译期的、僵化的;动态组合是运行期的。对象图很少与类图一致。
  • 为变化而设计:封装变化点。应用(application)、工具箱(toolkit)、框架(framework)是复用的三个层次——框架把应用的不变方面固化下来。

如何选择一个模式(1.7):按名字与意图浏览目录 → 查看相关模式并比较 → 检查模式在目标上下文中是否可行 → 简化假设/替换等价模式(如 Strategy 与 State 结构相似;Mediator/Strategy 常可用 Observer 实现)。

如何使用一个模式(1.8):确认适用性 → 从恰当名字开始命名参与方 → 声明接口 → 决定关键机制(先写伪码)→ 把设计分解进各参与方 → 评估并迭代权衡。

六、案例研究:文档编辑器 TexStar(第 2 章)

用一个贯穿案例演示模式如何自然浮现:

  • 文档结构:文档 = Component 递归组合(Composite 模式),字形 Glyph 作为最小叶子单元。
  • 排版:把排版算法封装为 Compositor,Strategy 模式使排版策略可替换。
  • 界面装饰:Border/ScrollBox 用 Decorator 给视图加边框/滚动条,避免类组合爆炸。
  • 格式化与规格:Spec 对象记录文档生成规格(Memento/Command 支撑撤销)。
  • 用户操作:菜单/快捷键触发命令 → Command 模式支持撤销;Glyph 遍历 → Visitor;用户交互流状态 → State;依赖更新 → Observer;字形共享 → Flyweight。
  • 结论:同一系统在十余个模式上交织,模式之间是协作关系而非孤立选项(如 Composite 常配 Iterator/Visitor;Prototype 常是 Abstract Factory 的替代)。

七、可行动点

  • 代码评审时对照两条原则提问:这里的变量声明成具体类了吗?这里的继承是”替代关系”还是”偷实现”?
  • 见到 if/switch 按类型分派 → 考虑 State / Strategy;见到一大坨一次性构造 → Builder;见到子系统散落调用 → Facade。
  • 学习顺序建议:先吃透 Strategy/Observer/Composite/Adapter/Decorator 五个高频模式,再按目录补全。
  • 本书示例基于 C++/Smalltalk;现代语言(Java/Python/C#)中部分模式的必要性下降(闭包替代 Command/Strategy 的一部分、语言内置单例机制、动态语言弱化 Iterator),但意图层面仍全部有效——评价”某模式已过时”时先区分是”意图”还是”某个语言的样板写法”过时。

关联