cv.cm

编码 · 设计模式

设计模式详解:23 种 GoF 设计模式

阅读约 9 分钟

简短回答

设计模式是针对软件设计中反复出现的问题所总结的、有名字的可复用解决方案,描述层次高于具体代码。最经典的模式目录是 1994 年由 Gamma、Helm、Johnson 和 Vlissides(即“四人帮”,Gang of Four,GoF)合著的《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software),书中列出 23 种模式,分为三类:5 种创建型、7 种结构型和 11 种行为型。设计模式为团队提供了共同的交流语言,但它们是解决特定问题的工具,而不是目标本身。

什么是设计模式

设计模式不是可以直接复制的库或代码片段,而是对一个反复出现的问题、解决方案的核心思路以及使用它的权衡取舍的描述。具体代码仍然需要你根据所用语言和自己的程序来编写。

GoF 一书用四个基本要素来描述每一种模式:

  • 名称:一两个词,方便开发者交流设计,例如“可以用装饰器(Decorator)把它包一层”。
  • 问题:何时应该使用这个模式,以及什么样的场景需要它。
  • 解决方案:涉及的类或对象、它们各自的职责以及协作方式。
  • 效果:使用后的收益与代价,例如以更多的类换取更高的灵活性。

这个思想来自建筑学。Christopher Alexander 在 1977 年出版的《A Pattern Language》中总结了建筑和城镇规划中反复出现的解决方案,GoF 的作者把同样的思路应用到了面向对象软件上。

四人帮(GoF)与那本书

《设计模式:可复用面向对象软件的基础》由 Addison-Wesley 于 1994 年出版。书中的示例使用 C++ 和 Smalltalk,但这些模式适用于任何面向对象语言,包括 Java、C#、TypeScript、Python 和 Kotlin。

除了模式目录,这本书还提出了贯穿大多数模式的两条通用原则:

  • 针对接口编程,而不是针对实现编程。代码应当依赖对象能做什么,而不是它的具体类。
  • 优先使用对象组合,而不是类继承。通过组合对象来构建行为,通常比构建很深的类继承层次更灵活。

书中还按作用范围对模式分类:类模式使用继承,在编译期就已确定(工厂方法、类适配器、解释器、模板方法);对象模式使用组合,可以在运行时改变(其余所有模式)。

按类别列出全部 23 种 GoF 模式

创建型模式关注对象如何创建;结构型模式关注如何把类和对象组合成更大的结构;行为型模式关注对象之间如何通信并分配职责。

模式类别一句话意图
Abstract Factory(抽象工厂)创建型创建一族相关对象,而无需指明它们的具体类。
Builder(生成器)创建型分步骤构造复杂对象,把构造过程与对象的表示分离。
Factory Method(工厂方法)创建型由子类决定实例化哪个类。
Prototype(原型)创建型通过复制已有实例来创建新对象。
Singleton(单例)创建型保证一个类只有一个实例,并提供全局访问点。
Adapter(适配器)结构型把一个接口转换成客户端所期望的另一个接口。
Bridge(桥接)结构型把抽象与实现分离,使两者可以独立变化。
Composite(组合)结构型用相同的方式对待单个对象和对象树。
Decorator(装饰器)结构型通过包装对象,动态地为其增加职责。
Facade(外观)结构型为复杂的子系统提供一个简单的统一接口。
Flyweight(享元)结构型共享公共状态,以高效支持大量细粒度对象。
Proxy(代理)结构型提供一个替身,用来控制对另一个对象的访问。
Chain of Responsibility(责任链)行为型让请求沿着处理者链传递,直到有一个处理者处理它。
Command(命令)行为型把请求封装成对象,使其可以排队、记录日志或撤销。
Interpreter(解释器)行为型用类表示简单语言的语法,并解释执行该语言的句子。
Iterator(迭代器)行为型按顺序访问集合中的元素,而不暴露其内部结构。
Mediator(中介者)行为型集中管理对象之间的通信,使对象不必直接相互引用。
Memento(备忘录)行为型在不破坏封装的前提下捕获并恢复对象的状态。
Observer(观察者)行为型当一个对象发生变化时,自动通知所有依赖它的对象。
State(状态)行为型让对象在内部状态改变时改变其行为。
Strategy(策略)行为型让一族算法可以在同一个接口之后互相替换。
Template Method(模板方法)行为型定义算法的骨架,把其中某些步骤交给子类实现。
Visitor(访问者)行为型在不修改类层次结构的前提下,为其增加新的操作。

实战中的模式:TypeScript 策略模式

下面是一个模式实际应用的小例子。结账流程需要用不同的方式计算运费。与其写一长串不断增长的 if/else,不如把每条规则做成同一个接口背后的可互换对象:

interface ShippingStrategy {
  cost(weightKg: number): number;
}
class FlatRate implements ShippingStrategy {
  cost(): number { return 5; }
}
class PerKilogram implements ShippingStrategy {
  cost(weightKg: number): number { return 2 * weightKg; }
}
class Checkout {
  constructor(private shipping: ShippingStrategy) {}
  setShipping(s: ShippingStrategy): void { this.shipping = s; }
  total(itemsPrice: number, weightKg: number): number {
    return itemsPrice + this.shipping.cost(weightKg);
  }
}
const checkout = new Checkout(new FlatRate());
console.log(checkout.total(40, 3)); // 45
checkout.setShipping(new PerKilogram());
console.log(checkout.total(40, 3)); // 46

在 TypeScript 中,函数本身就是值,所以同样的设计可以用更少的代码写出来。这是一个常见的规律:在支持头等函数(first-class function)的语言里,许多模式会变得更简单,甚至消失。

type Shipping = (weightKg: number) => number;
const flatRate: Shipping = () => 5;
const perKilogram: Shipping = (w) => 2 * w;
function total(itemsPrice: number, weightKg: number, shipping: Shipping): number {
  return itemsPrice + shipping(weightKg);
}
console.log(total(40, 3, flatRate));    // 45
console.log(total(40, 3, perKilogram)); // 46

如何选择设计模式

从问题出发,而不是从模式出发。只有当一个模式能消除代码中真实存在的痛点时,它多出来的那些类才物有所值。下面这些问题可以帮你找到常见的候选模式:

如果你的问题是……可以考虑
代码不应依赖它所创建的具体类工厂方法、抽象工厂
构造函数有很多可选参数生成器(Builder)
需要把现有的类适配到另一个接口适配器(Adapter)
复杂的库需要一个简单的入口外观(Facade)
想给对象增加功能,又不想子类数量爆炸装饰器(Decorator)
要处理树形结构,例如菜单、文件或 UI 组件组合、访问者、迭代器
一个对象变化时,许多部分要做出反应观察者(Observer)
行为取决于模式或状态,分支很多状态(State)
需要可互换的算法或规则策略(Strategy)
操作需要撤销、排队或记录日志命令、备忘录
  1. 先写出最简单、能正常工作的代码。
  2. 当同一种修改反复带来麻烦时(例如每增加一种支付方式就要修改五个文件),把这个问题明确地说出来。
  3. 查找针对该问题的模式,并阅读它们的效果与代价。
  4. 只应用能消除痛点的最小版本的模式,到此为止。

批评与局限

设计模式很有用,但经常被误用。下面这些常见的批评值得了解:

  • 过度使用:“以防万一”而添加工厂、接口和管理类,会让简单的代码难以阅读。这有时被称为“模式狂热”。
  • 语言特性缺失:1996 年,Peter Norvig 指出 23 种 GoF 模式中有 16 种在 Lisp 和 Dylan 等动态语言中不需要或更简单。头等函数可以取代策略和命令,内置迭代器可以取代迭代器模式。
  • 尤其是单例,当它被当作全局状态使用时,被普遍认为是一种反模式,因为它隐藏了依赖关系,也让测试更困难。
  • 这套目录出自 1994 年。后来的实践又产生了书中没有的其他知名模式,例如依赖注入、仓储(Repository)以及各种 Model-View-Controller 变体。

这本书长久的价值在于它提供的词汇,以及权衡取舍的思维习惯。说一句“这是个适配器”,两个词就能向其他开发者传达大量信息。

相关原则

设计模式通常与面向对象设计的 SOLID 原则一起讲解。例如,策略和装饰器是遵循开闭原则的直接方式,抽象工厂是应用依赖倒置原则的常见做法。学习这些原则,有助于理解模式为什么是现在这个样子。

常见问题

设计模式一共有多少种?

四人帮的书中收录了 23 种模式:5 种创建型、7 种结构型和 11 种行为型。此后又出现了许多其他模式,例如针对并发、企业应用和用户界面的模式。

应该先学哪些设计模式?

先学实际代码中最常遇到的:策略、观察者、工厂方法、装饰器、适配器和外观。然后再学习生成器、组合和命令。

设计模式现在还有意义吗?

有,既可以作为共同的交流语言,也可以作为反复出现的问题的解决方案。有些模式如今已内置于语言和框架中,你无需自己写出完整的类结构就能使用它们。

设计模式只适用于面向对象语言吗?

GoF 模式是为面向对象语言编写的,但其背后的思想无处不在。在函数式语言中,许多模式会变成简单的高阶函数。

设计模式和算法有什么区别?

算法是解决计算问题(例如排序)的一组精确步骤。设计模式描述的是如何组织代码和职责,需要根据每个程序的具体情况进行调整,而不是照着步骤执行。

全部编码教程