《实现模式》——Implementation Patterns
作者:Kent Beck 译者:李剑、熊节、郭晓刚 出版:人民邮电出版社,2009年1月
核心观点
好的代码首先是给人读的,其次才是给机器执行的。
Kent Beck 在本书中将自己多年的编程习惯和阅读代码的经验,凝练成一套完整的编程理论:价值观 → 原则 → 77 种实现模式。这不是一本风格指南,也不是一本设计模式书,而是关于”如何用代码与他人沟通”的书。
任何一个傻瓜都能写出机器能懂的代码。好的程序员应该写出人能懂的代码。 —— Martin Fowler(引自本书译者序)
三层结构:价值观 → 原则 → 模式
第一层:价值观(3 个)
价值观是编程的统一支配性主题,影响每个决策,但难以直接应用。
| 价值观 | 核心含义 |
|---|---|
| 沟通 | 代码的首要目的是向其他开发者传达意图。程序更多的时候是被阅读,而不是被编写。 |
| 简单 | 去掉代码中多余的复杂性。“不要过早优化,但也不要过早劣化。“ |
| 灵活 | 保持开放的心态,代码应该易于适应未来的变化。没有”完工”一说,修改的投入远大于最初编写的投入。 |
第二层:原则(6 个)
原则介于价值观和模式之间——比价值观更贴近编程场景,比模式更通用。在没有现成模式可用时,可以用原则推导解决方案。
| 原则 | 含义 |
|---|---|
| 局部化影响(Local Impact) | 一个变化应该只影响局部范围,避免牵一发而动全身。 |
| 最小化重复(Minimize Duplication) | 重复是万恶之源,每一处重复都是未来修改时的潜在陷阱。 |
| 将逻辑与数据捆绑(Bind Logic and Data) | 数据和操作它的逻辑应该放在一起,这是面向对象的核心思想。 |
| 对称性(Symmetry) | 代码在结构上应该对称——对称的代码更容易理解和预测。 |
| 声明式表达(Declarative Expression) | 尽可能用”是什么”而非”怎么做”来表达意图。 |
| 变化率(Rate of Change) | 将一起变化的东西放在一起,变化频率不同的东西分开。 |
第三层:实现模式(77 种)
模式是可以直接套用的具体编程实践。以下按章节分类:
第 5 章:类(Class)
类是面向对象编程的基本组织单元。本章讨论如何设计类,使其清晰表达意图。
| 模式 | 核心思想 |
|---|---|
| 简单的超类名 | 超类用简洁的名词命名(如 List),不需要前缀描述其抽象程度。 |
| 限定性的子类名 | 子类用修饰词 + 超类名的方式命名(如 ArrayList),一眼看出继承关系。 |
| 抽象接口 | 接口描述”能做什么”,而不是”是什么”。接口名应该是形容词或动词短语。 |
| interface | Java 中用 interface 定义契约,实现可以有多个。 |
| 抽象类 | 当有公共实现需要复用时用抽象类,否则优先用 interface。 |
| 有版本的 interface | 接口演化时,发布新版本接口而非修改旧接口,保持向后兼容。 |
| 值对象 | 不可变对象,相等性由内部值而非身份决定。大量使用值对象可以降低状态管理复杂度。 |
| 特化 vs 子类 | 特化是”是一种”关系(用继承),子类是”复用代码”的手段——两者不要混淆。 |
| 实现器(Implements) | 用 implements 声明实现的接口,清晰表达契约。 |
| 内部类 | 当一个类只在另一个类内部使用时,定义为内部类,减少命名空间污染。 |
| 实例特有的行为 | 不同实例有不同行为时的几种实现方式(见下方) |
| 条件语句 | 最简单直接的方式:if/else 判断实例类型 → 但违反开闭原则。 |
| 委派 | 将变化的部分委派给另一个对象(策略模式)。 |
| 可插拔的选择器 | 在运行时动态选择方法(反射/函数字典)。 |
| 匿名内部类 | 用于一次性的小回调,避免创建大量一次性命名类。 |
| 库类 | 不要修改框架/库类,用组合或工具类扩展。 |
第 6 章:状态(State)
译者序中特别强调:“至少应该读完第 6 章’状态’。因为在各种常见的低级错误中最常见的就是关于’什么信息在什么地方’的决策错误。“
状态的访问方式
| 模式 | 适用场景 |
|---|---|
| 直接访问 | 直接读写字段。简单直接,但暴露了内部实现。 |
| 间接访问 | 通过 getter/setter 访问。增加了一层间接,可以在访问时加入逻辑(如延迟计算、校验)。 |
状态的可变性
| 模式 | 说明 |
|---|---|
| 可变状态 | 默认选择,但需要谨慎管理状态变更。 |
| 不可变状态 | 一旦创建就不能修改(值对象)。线程安全、易于推理,但创建新对象有开销。 |
变量的分类
| 类型 | 生命周期 | 作用域 |
|---|---|---|
| 局部变量 | 方法调用期间 | 方法内部 |
| 字段 | 对象存活期间 | 整个对象 |
| 参数 | 方法调用期间 | 方法内部,从外部传入 |
| 收集参数 | 在多个方法间传递、累积结果的参数对象 |
其他状态模式
- 可选参数:方法有多个参数时,用重载或参数对象简化调用
- 变长参数:参数数量不确定时用
... - 参数对象:当参数超过 3 个时,封装成对象传递
- 常量:用
static final声明的不变值 - 按角色命名:变量名应反映其在逻辑中的角色,而非其类型
- 声明时的类型:变量的声明类型应该尽可能抽象(面向接口编程)
- 及早初始化:在声明时就初始化,避免 null 问题
- 延迟初始化:第一次使用时才初始化,用于创建成本高且不一定用到的对象
第 7 章:行为(Behavior)
控制流的几种形式
| 模式 | 说明 |
|---|---|
| 主体流 | 方法的主流程应该清晰可见,把异常情况和错误处理推到边缘。 |
| 卫述句(Guard Clause) | 用提前返回处理异常情况,减少嵌套,让主流程更清晰。 |
消息模式
方法调用本质是对象之间传递消息。
| 模式 | 说明 |
|---|---|
| 选择性消息 | 根据条件选择调用不同方法(多态是其中一种形式)。 |
| 双重分发 | 两个对象的类型都影响行为时使用(如 Visitor 模式)。 |
| 分解性消息 | 把一个大方法拆成多个依次调用的小方法。 |
| 反置性消息 | 把调用关系反过来——“你调用我”变成”我回调你”。 |
| 邀请性消息 | 调用方邀请被调用方参与计算,而非命令式地获取数据。 |
| 解释性消息 | 给复杂的逻辑块起一个好的方法名,用方法名解释代码在做什么。 |
异常流
| 模式 | 说明 |
|---|---|
| 异常 | 用异常处理错误,不要用返回值表示错误。 |
| 已检查异常 | 对于调用方应该且能够处理的异常用 checked exception。 |
| 异常传播 | 对于无法处理的异常,直接抛出(或包装后抛出),不要吞掉。 |
第 8 章:方法(Method)
方法设计原则
| 模式 | 核心思想 |
|---|---|
| 组合方法(Composed Method) | 一个方法应该由几个同级抽象层的子步骤组成,每个步骤有清晰的命名。 |
| 揭示意图的名称 | 方法名应该清楚地说明”做什么”,而不是”怎么做”。 |
| 方法可见性 | 公开方法越少越好,能用 private 就不用 protected,能用 protected 就不用 public。 |
| 方法对象 | 当一个方法太复杂时,把它提升为一个对象,用多个小方法和字段组织逻辑。 |
方法类型
| 分类 | 典型模式 |
|---|---|
| 转换 | 转换方法(toString()、toArray())、转换构造器 |
| 创建 | 完整的构造器(构造后对象立即可用)、工厂方法、内部工厂 |
| 访问 | Getting 方法、Setting 方法、查询方法(返回 boolean,用 is/has/can 前缀) |
| 布尔值 Setting 方法 | 用两个命名方法替代 setState(boolean),如 enable() / disable() |
| 相等性判断 | equals() 和 hashCode() 必须同时正确实现 |
| 安全副本 | 返回可变对象的副本,防止外部修改内部状态 |
| 容器访问器方法 | 不要直接返回内部容器引用,返回不可修改视图或副本 |
第 9 章:容器(Collections)
容器接口的选择
| 接口 | 适用场景 |
|---|---|
| Array | 大小固定、性能敏感时使用。 |
| Iterable | 只需要遍历,不需要知道具体类型。 |
| Collection | 一组元素,不关心顺序和重复。 |
| List | 有序、允许重复。 |
| Set | 不允许重复。 |
| SortedSet | 有序的 Set。 |
| Map | 键值对映射。 |
容器实现的选择
选择实现类时需要考虑:性能特征(读多还是写多)、是否需要排序、内存开销等。
Collections 工具类
- 查询(binarySearch 等)
- 排序(sort)
- 不可修改的容器(unmodifiableList/Set/Map)——返回只读视图
- 单元素容器(singletonList/Set/Map)
- 空容器(emptyList/Set/Map)——比 null 更好的选择
继承容器
尽量不要继承容器类来扩展功能,优先使用组合。
第 10 章:改进框架
讨论框架演化的艺术——如何在修改框架的同时不破坏已有的应用代码。
核心困境
框架需要不断改进以适应新需求,但每个修改都可能破坏已有的使用者。
不兼容的更新
有些修改必然不兼容,需要明确通知使用者并提供迁移路径。
鼓励可兼容的变化
- 程序库类:框架中的类应该设计得让使用者可以方便地扩展,而不需要修改框架本身
- 对象:把扩展点设计在对象层面,通过组合/继承/回调等方式扩展
附录 A:性能度量
提供了一套性能度量的工具和方法,用来测量不同实现方式的实际性能差异。
核心观点:不要凭感觉优化,先测量。 很多”常识”中的性能结论在实际场景中并不成立。
包含对各种容器实现(ArrayList vs LinkedList、不同 Set、不同 Map)的性能比较。
关键概念提炼
1. 代码即沟通
全书最核心的思想:代码的主要读者是人,不是编译器。编写代码时应该始终想着”下一个读这段代码的人能不能快速理解我的意图”。
2. 模式是压力的解决方案
每个模式背后都有一系列”压力”(force)——可读性、性能、灵活性、简洁性之间的权衡。理解了压力,才能在合适的场景选用合适的模式。
3. 从价值观到模式的三级跳
- 价值观告诉你方向(要沟通、要简单、要灵活)
- 原则告诉你路径(局部化影响、消除重复……)
- 模式告诉你具体怎么走(77 种可直接套用的实践)
当你遇到模式没覆盖的场景时,回到原则;当原则也无法判断时,回到价值观。
4. 命名是最重要的实现模式
好的命名本身就是文档。揭示意图的名称、按角色命名、简单的超类名、限定性的子类名——命名模式占据了本书很大篇幅。
5. 状态管理是低级错误的重灾区
译者序特意强调了第 6 章。“什么信息放在什么地方”——变量的作用域、生命周期、可变性——是编程中最常见的低级错误来源。
与其他知识的关联
- 《重构》-改善既有代码的设计 —— 姊妹篇。《重构》讲如何修改已有代码,《实现模式》讲如何从一开始就写出好代码。两者共享”代码可读性”的核心价值观。
- 《SICP》-计算机程序的构造和解释-第二版 —— 从更基础的层面讨论程序的本质。实现模式是工程层面的实践,SICP 是理论层面的奠基。
- 《程序员修炼之道》-从小工到专家 —— 更广泛的程序员职业素养,实现模式是其中”编码”这一维度的深入展开。
- 《代码整洁之道》 —— 主题高度重合,但本书更结构化(价值观→原则→模式三层),更偏 Java 语言层面的具体实践。
- Kent Beck 的另一部著作《测试驱动开发》(TDD)—— 实现模式和 TDD 是同一套编程哲学的两个侧面。
适合谁读
- 初学者:建立正确的编程审美观,从一开始养成好习惯
- 资深工程师:把已有的直觉经验系统化,理解”为什么要这样写”
- 技术 Leader:作为团队代码规范的理论基础,比纯规则的代码规范更有说服力
“如果你确实没有时间,至少应该读完第 6 章’状态’。” —— 译者序