书 · 1994-10 · 埃里希·伽玛、理查德·赫尔姆、拉尔夫·约翰逊、约翰·弗利塞德斯

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

计算机与 AI书单第 23 名

一句话结论

把软件中容易变化的部分隔离起来,让修改尽量少牵动其他代码;23种模式记录了实现这件事的不同办法和代价。

值不值得读完整本?

值得完整读过,尤其适合已经写过面向对象程序、开始遇到维护困难的人。它能训练你判断依赖和变化的能力,适合分两轮读,并在项目中反复查阅。

  • 先读第1章“引言”,弄清接口、实现、继承、组合,以及模式的适用条件和代价;如果这些概念还陌生,先补面向对象编程基础。
  • 接着细读第2章“设计文档编辑器”,跟着Lexi案例看多个模式如何协作,避免把每个模式理解成孤立的类图。
  • 第3至5章分别是创建型、结构型、行为型模式。第一轮读完23种模式的意图、适用性和效果,第二轮重点比较抽象工厂与工厂方法、适配器与桥接、策略与模板方法等容易混淆的模式。
  • 读第6章结论。第一轮可以略读重复的C++、Smalltalk语法细节;附录按需查阅,涉及实现取舍的代码仍值得回看。
  • 配合练习:选一个小程序,先实现直接方案,再引入一个模式,实际尝试更换算法、增加产品族或增加操作。记录哪些修改变得局部化,又增加了哪些对象和调用关系。

核心观点

1. 模式复用的是设计经验,并提供共同语言

设计模式记录反复出现的问题、解决结构及其适用条件。理解一个模式,需要同时理解名称、问题、解决方案和效果。

为什么:许多设计困难会在不同项目中重复出现,但具体业务和类名会掩盖它们的相似性。把对象的职责和协作关系抽象出来,读者就能迁移已有经验,少走重复试错的弯路。模式名称还让团队能够简洁地讨论设计意图,而适用条件和效果提醒大家检查这种经验是否适合当前问题。

2. 围绕变化设计,让调用者依赖接口

先找出哪些部分可能变化,再把它们放到可替换的边界后面。调用者应依赖对象提供的操作,而尽量少依赖具体实现。

为什么:如果算法、平台或具体类的细节散落在调用代码里,每次替换都需要同时修改许多地方。稳定的接口把调用者需要的能力与实现这些能力的方式分开,使修改可以集中在边界的一侧。不同实现只要满足调用者的要求,就能被替换使用。这里的接口是对象对外提供的操作约定,并不限定为某种语言的interface关键字。

3. 优先考虑对象组合,并权衡间接调用的代价

复用功能时,应优先考虑让对象持有并使用其他对象,而不是不断通过实现继承扩展类。组合和继承可以配合,选择取决于需要保留什么、改变什么。

为什么:实现继承容易让子类依赖父类内部细节,父类修改后,影响可能沿着继承关系传播。组合通过对象接口连接功能,可以保持各个类的封装,并允许替换参与协作的对象。这样常能减少为了不同功能组合而建立的大量子类。代价是对象和调用关系增多,所以新增的灵活性必须足以抵偿理解和运行成本。

4. 创建型模式隔离对象创建中的变化

对象如何产生,也会形成依赖。创建型模式把实例化的选择和过程从使用对象的代码中分离出来。

为什么:到处直接指定具体类,会把使用逻辑与某一种配置绑在一起,换实现时就得四处修改。工厂方法把实例化选择交给子类,抽象工厂则把相关产品作为一个家族统一创建。生成器分离构造过程与最终表示,原型通过复制已有对象创建实例,单例控制实例数量及访问入口。它们处理的变化不同,因此不能因为都与创建有关,就把它们当作可以互换的写法。

5. 结构型模式安排边界,使已有部件能够协作

结构型模式处理类和对象如何组成更大的结构。关键是辨认障碍究竟来自接口不兼容、层次结构、功能扩展,还是访问和存储方式。

为什么:现成部件可能已经具有所需功能,却因为接口不同而无法直接使用,适配器通过转换接口保留这部分价值。桥接把抽象与实现分开,使两边可以独立扩展;组合让调用者以相近方式处理单个元素与元素集合。装饰器通过包装增加职责,外观简化对子系统的使用,代理控制访问,享元通过共享适合共享的状态降低存储成本。相似的包装结构可能服务于不同目的,所以选择模式要看它消除了哪一种依赖。

6. 行为型模式重新分配职责,隔离算法与协作方式

行为型模式关注工作由谁完成、请求如何传递,以及算法和对象状态如何变化。它们让变化集中到明确的职责中。

为什么:策略把可替换算法交给独立对象,模板方法则保留算法骨架,让子类改变部分步骤。命令把请求封装成对象,使发出请求与执行请求分离,并为排队、记录和撤销提供组织方式。观察者、中介者和职责链分别调整通知、协调和请求分派,减少参与者之间的直接绑定。迭代器分离遍历与容器表示,访问者集中对元素的操作,但访问者也会使增加新元素类型变得麻烦。这些模式把复杂性转移到协作关系中,因此仍需检查是否值得增加这层间接性。

反对意见

部分模式的复杂性来自语言限制(Peter Norvig)

Norvig在1996年的《Design Patterns in Dynamic Programming》中指出,在他考察的Lisp或Dylan实现中,23种模式有16种至少在某些用法下可以明显简化。一等函数、类型和宏等机制能够承担原本需要额外类与对象完成的工作。因此,不能把书中的实现结构当成跨语言都必须保留的形式;这也不意味着这些设计问题全部消失。

过早引入模式会增加无用复杂性(Martin Fowler)

Fowler在《Is Design Dead?》中批评刚学会模式就到处套用的做法:增加灵活性需要付出成本,应在确有需要时逐步演化到模式,发现收益不足时也应移除。他仍把模式视为重要的设计知识。这个批评主要针对应用时机,而且原书第1章本身也提醒读者避免滥用。

模式结构未必能把实现代码集中起来(Jan Hannemann、Gregor Kiczales)

两人在2002年的《Design Pattern Implementation in Java and AspectJ》中比较23种模式的Java与AspectJ实现,报告其中17种出现模块化改善。例如,观察者的通知逻辑可能分散在多个参与类中,面向方面的机制可以把部分协作代码集中起来。这说明传统面向对象实现仍有模块化局限;该比较并没有证明AspectJ在所有项目中都更好。

相关推荐

导读由 AI 根据公开资料整理,没有读取原书全文,一切以原著为准。导读 Primer 首页