速成课 · No. 04
它们当中大多数,你早已徒手重新发明过了。一个模式不过是个名字——好让一个团队用一个词说清原本要一整段话才说得明白的事,并在把某一步做砸之前就认出它。
20 个模式 · 每个配图示 · 结尾有速查表
设计模式是对一个在代码里一再出现的问题的、有名字的可复用答案。它是词汇,不是库——你不去安装它,你认出它。
Design Pattern——给你早已会做的那一步起的名字
球员不会在比赛中途现场发明「撞墙配合」。它有个名字,而说出这个名字,比把整套动作描述一遍要快。
设计模式是称职的开发者无论如何都会徒手重新发明的一步。给它起个名字,就把一段解释变成了全队共享的一个词。当有人说「用 Decorator 包一层」,所有人立刻看到那个形状——这就是模式全部的意义:一套共享的词汇,用来指称你本来就会做的那些步骤。
- 是什么
- 对代码中反复出现的问题的、有名字的解法。
- 不是什么
- 不是库:你不去安装它,你认出它,然后自己写。
- 尺度
- 活在单个模块的代码里——不同于架构模式,后者决定整个系统的平面图。
- 为什么
- 共享语言:一个词代替一段话。
Gang of Four——模式是被发现的,不是被发明的
没有人发明拱。建造者们一再发现它是跨越空隙最牢固的办法——于是它有了名字。
Gang of Four(GoF,四人组)是《Design Patterns》一书的四位作者。他们并没有设计出 23 个巧妙的花招;他们是把好代码里一再出现的解法编成了目录。所以模式不是要去命中的目标——它是问题需要时你自然而然走到的形状。如果你是硬塞进去的,多半是你还没遇到那个问题。
地图:三个家族,三个问题
厨房里有三类活的工具:做菜、装盘、上菜。模式也是这样划分的。
创建型模式讲的是对象怎么诞生——谁来构造它们,怎么构造。结构型模式讲的是对象怎么拼成更大的形状。行为型模式讲的是对象怎么对话、怎么分担责任。几乎每个经典模式都是这三个答案之一。
Indirection——你用不着的模式只是复杂度
口袋里的瑞士军刀很顺手。把二十件工具全拧到你家大门上,就不然了。
每个模式都会增加间接层(indirection)——你和那个东西之间多出一层。当它换来的灵活性你真会用到时,这值得;不会用到时,就是纯粹的代价。常见的错误不是不懂模式,而是在问题还不存在时就伸手去拿一个。
怎么读每个模式
护照:每本的栏目都一样,所以很容易比较。
从这里开始,每个模式都有相同的几个部分:它的英文名称(那就是术语——记住它)、一幅取自日常生活的画面、一张图示,以及一张带栏目的卡片:问题(哪里疼)、思路(怎么解决)、例子、代价(你付出什么)、何时使用,以及关联模式。结尾有一张把所有名称放在一页上的速查表。
模式是对话的捷径。用它是为了被人听懂,不是为了显得聪明。
对象如何诞生——而不必把 new 撒得到处都是,也不把你的代码焊死在具体的类上。
Factory——按名字要东西,而不是按配方
咖啡机:你按下「拿铁」,就得到一杯。你不用自己研磨、打奶泡、倒——也不需要知道它是怎么做的。
Factory(工厂)把对象交给你,而不用你直接调用它的构造函数,所以调用方的代码从不依赖具体的类。它只认识接口。它换来的是自由:可以改变被构造的东西,而不必去动提出请求的那一方。
- 问题
new StripeProvider()散落在代码各处——被焊死在一个类上。- 思路
- 向工厂要对象;由它决定构造哪个类。
- 例子
createPaymentProvider("stripe")返回一个PaymentProvider。换成 PayPal,只需改工厂,不用改每一处调用。- 代价
- 多一层。一个只构造一样东西的工厂是坏味道。
- 何时使用
- 有好几种实现,或者将来会有。
- 关联
- Abstract Factory——同一个思路,用于一整族对象。
Abstract Factory——一次造出一整套相配的家族
买一套家具——「维多利亚风」或「现代风」——每一件都是同一种风格,不会有不配套的椅子。
当对象必须成族地保持一致时,**Abstract Factory(抽象工厂)**一次造出整套,让各部分彼此契合。很强大——但它是重型机械,只有当这些家族是真实存在的时候才值得。
- 问题
- 对象之间必须相配,而且很容易搞混。
- 思路
- 一个工厂以一种风格造出整套对象。
- 例子
一个 UI 工具包,用同一个工厂为 macOS 或 Windows 造出相配的按钮、菜单和对话框。
- 代价
- 重型机械:大量的接口和类。
- 何时使用
- 只有当家族是真实的,而且不止一个时。
- 关联
- 由 Factory 构成。「只服务一个产品的抽象工厂」是典型的过度设计。
Builder——一步一步组装复杂对象
点一份定制汉堡——面包、肉饼、不要洋葱、加双份奶酪——一次选一样,最后说「好了」。
当一个对象有许多可选部分时,**Builder(建造者)**让你一块一块地设置,而不是用一个带十二个参数的怪物构造函数。最后调用 build(),拿到成品。它把一个令人费解的构造函数换成了一条可读的链。
- 问题
- 一个十二个参数的构造函数,其中一半是可选的。
- 思路
- 一次设置一个部分,最后
build()。 - 例子
QueryBuilder().select("name").where("age > 18").limit(10).build()。- 代价
- 真正的类旁边多出一个类。
- 何时使用
- 对象有许多可选部分。
- 关联
Factory 决定构造_什么_;Builder 决定_怎样_组装复杂的东西。
Singleton——有且只有一个,人人共用
一个国家有一位总统。所有提到「总统」的人指的都是同一个人——不会有两个。
Singleton(单例)保证只有一个实例和一个全局访问点。但要小心:单例是穿着漂亮外套的全局状态,而全局状态会让测试和推理变难。
- 问题
- 你需要恰好一个共享实例。
- 思路
- 由类本身保证一个实例和一个访问点。
- 例子
- 所有人都从中读取的配置对象或日志器。
- 代价
- 全局状态:测试和推理都变得更难。
- 何时使用
- 很少。一个你显式传入的单一实例,往往胜过通过全局访问到的那个。
- 关联
- 和 Factory 一样属于创建型,但关心的是「几个」,而不是「哪一个」。
Prototype——复制一份填好的,而不是从头新建
复印一张已经填好的表,比从空白表格从头填起要快。
当从零开始造一个对象既昂贵又琐碎时,Prototype(原型)让你把现有的对象克隆一份再稍作调整。适用于构造成本高、而且大多彼此相似的对象。
- 问题
- 从零构造这个对象既慢又琐碎。
- 思路
- 拿一个现成的对象,复制它,调整差异的部分。
- 例子
- 复制一个已配置好的游戏敌人,或一个带样式的文档节点。
- 代价
- 复制需要小心:哪些被复制,哪些仍是共享的。
- 何时使用
- 对象构造成本高,而且大多相似。
- 关联
- Factory 的一个替代方案:不是「构造」,而是「复制」。
把
new藏起来。知道对象怎么构造的地方越少,你改它就越自由。
对象如何组合成更大的结构——把不兼容的部分拼在一起,不重写就能增加能力。
Adapter——两个接口之间的转换插头
你的笔记本充电器到了国外——设备没问题,插座也没问题;只差中间那个小转换器。
**Adapter(适配器)**包装一个接口,让它看起来像另一个,使两个本来不是为了合作而设计的东西能一起工作。这是你阻止别人的 API 渗透到整个代码库的办法。
- 问题
- 别人的接口和你的对不上,而且在你的代码里四处蔓延。
- 思路
- 一个包装层,把调用从一个接口翻译到另一个接口。
- 例子
- 把第三方支付 SDK 包装起来,使它满足你自己的
PaymentProvider。 - 代价
- 供应商的 API 变化时,又多一层要更新。
- 何时使用
- 接入第三方代码或遗留代码。
- 关联
- 像 Decorator 和 Proxy 一样包装对象,但它会改变接口。
Decorator——加配料,而不改披萨本身
一张普通披萨,再加奶酪,再加蘑菇——每一层都包住上一层并添加点什么,它仍然是披萨。
**Decorator(装饰器)**包装一个对象来添加行为,既不修改原对象,也不去继承它。接口保持不变,所以各层可以叠加。它让你以没人需要事先预料的组合方式添加功能。
- 问题
- 你需要任意组合的附加功能;为每种组合写一个子类无法扩展。
- 思路
- 包住对象,添加一种行为,保持接口不变。
- 例子
- 数据流 → 压缩 → 加密;每一层都可以叠加或移除。
- 代价
- 许多小包装;一叠层层包裹更难调试。
- 何时使用
- 能力需要在运行时组合。
- 关联
和 Proxy 形状相同,但目的是添加,不是控制。组合优于继承的教科书式案例。
Facade——杂乱后台前的一个简单前台
酒店礼宾员——你说「我需要一辆出租车和一个晚餐预订」,柜台后面那一团电话由他们去处理。
Facade(外观)为复杂的子系统提供一个简单、单一的入口,隐藏它那些活动部件。它把一大片接口面缩成大多数调用方只需要的那一扇门。
- 问题
- 做一件简单的事,却要了解十几个子系统类。
- 思路
- 一扇简单的门,统筹调度整个子系统。
- 例子
VideoConverter.convert(file, "mp4")在背后默默调度编解码器、缓冲区和音频流。- 代价
- 复杂度并没有消失——只是被藏了起来。精细控制仍然要绕过外观去做。
- 何时使用
- 大多数调用方只需要一两种简单场景。
- 关联
- Adapter 改变一个东西的接口;Facade 简化整个子系统。
Proxy——控制访问的替身
名人的助理——你不能直接找到明星;助理负责筛选、安排日程,有时还替他们回答。
Proxy(代理)站在对象前面控制对它的访问——用于懒加载(lazy loading)、权限、缓存,或与远程的东西通信。同样的接口,前面多了一道门。
- 问题
- 对象加载昂贵、需要权限,或者位于另一台机器上。
- 思路
- 一个接口相同的替身,站在真实对象前面。
- 例子
一个图片代理,在真正展示之前不加载真实文件;一个访问代理,在转发调用之前先检查权限。
- 代价
- 每次调用多一跳;延迟对调用方是隐藏的。
- 何时使用
- 懒加载、权限、缓存、远程对象。
- 关联
- 和 Decorator 形状相同,目的不同——访问控制。
Composite——把一棵树和一片叶子同等对待
一个文件夹里有文件和其他文件夹。「计算大小」无论问一个文件还是整棵树,做法都一样。
**Composite(组合)**让你统一对待单个对象(叶子)和一组对象(组合体)——它们共用一个接口。它把递归结构变成了你无需在每一层都做特殊处理就能操作的东西。
- 问题
- 处理一棵树,每一层都需要一个特殊情况。
- 思路
- 叶子和分支共用一个接口;分支只是去调用它的子节点。
- 例子
- 一个 UI,按钮和装满按钮的面板都能响应
render()和getSize()。 - 代价
- 共用的接口可能过于笼统:并非每个操作对叶子都有意义。
- 何时使用
- 数据天然就是一棵树:文件夹、UI、文档。
- 关联
- 用 Iterator 遍历;用 Visitor 添加新操作。
组合优于继承:在运行时把行为拼装起来,而不是把它冻结在一棵类继承树里。
行为型模式的上半部分:对象如何分担工作、彼此对话——让责任划分干净,让正确的代码在正确的时刻运行。
Strategy——换掉算法,调用方不变
一个地图应用:同一段行程,你选「最快」「最短」或「避开收费路段」,它就规划出不同的路线。你选策略;应用只是照着走。
Strategy(策略)让算法在运行时可以互换,调用方保持不变而方法可以变化。在现代语言里,这往往只是传入一个函数,而不是一整个类——这正是重点:模式是那个思想,不是那套仪式。
- 问题
- 围绕同一算法的各种变体,
if阶梯越长越多。 - 思路
- 算法是接口背后一个可替换的对象(或函数)。
- 例子
- 一个接收
PricingStrategy的结账流程:普通价、会员价或节日促销价。 - 代价
- 给一个永不变化的算法套上 Strategy 是累赘。
- 何时使用
- 有两个或更多变体,在运行时选择。
- 关联
- 和 State 形状相同。在现代代码里,常常只是一个函数。
Observer——订阅,变了就通知你
一份新闻简报——你订阅一次,每一期新刊都会送到你手上。出版方不会给每位读者打电话;它只是发给名单。
在 Observer(观察者)中,订阅者向主题注册,主题一变化就通知它们——而主题并不知道它们是谁。它是事件驱动和响应式代码的骨干。
- 问题
- 一个对象必须把变化告诉许多其他对象,却不能按名字认识它们。
- 思路
- 一份订阅者名单 + 每次变化时
notify()。 - 例子
电子表格的一个单元格刷新所有依赖它的图表;UI 框架对状态做出反应;到处都是 pub/sub。
- 代价
- 通知链很难追踪;被遗忘的订阅会永远存活。
- 何时使用
- 若干相互独立的部分要对同一个变化做出反应。
- 关联
事件驱动代码的基础——和 Event-driven 架构是一回事,只不过发生在一个程序内部。
Command——把请求包装成对象
餐厅的点单——「5 号桌,两杯咖啡」这个请求变成了一张单子,你可以排队、重排、记录,甚至撕掉。
Command(命令)把一个动作变成一个独立的对象,于是你可以把它排队、记录、定时,或者撤销。这是让一个动作变成可以存储、可以重放的东西的办法。
- 问题
- 一个动作需要被延迟、重复、记录或撤销。
- 思路
- 把动作打包进一个对象,里面带着执行它所需的一切。
- 例子
编辑器里的每次编辑都是一个命令——因此有了撤销/重做和宏。作业队列就是等着轮到自己的命令。
- 代价
- 每个动作一个对象;每个命令的撤销都要单独想清楚。
- 何时使用
- 撤销/重做、队列、宏、动作日志。
- 关联
- 常与 Memento 搭配来实现撤销。
Chain of Responsibility——一路传下去,直到有人处理
客服的分级——前台先试,升级给专家,再升级给经理,直到有人真能解决。
在 Chain of Responsibility(责任链)里,请求沿着一条处理者链传递;每个处理者要么处理它,要么传给下一个。它让发送者与最终干活的人解耦。
- 问题
- 发送者不应该知道究竟是谁来处理请求。
- 思路
- 一条处理者链:每个做自己那一份,或者传下去。
- 例子
- Web 中间件:先认证,再日志,再限流,最后才是路由本身。
- 代价
- 各环的顺序很重要;请求可能走完整条链也没人处理。
- 何时使用
- 在主要工作之前,有若干相互独立的检查或步骤。
- 关联
- 在 Web 上你几乎总是以 middleware 的形式遇到它。
好的行为型模式让两个部分能够合作,而不让任何一方把另一方写死在自己里面。
行为型模式的下半部分:对象的行为如何随状态变化,你如何逐个遍历集合,以及如何固定一副骨架、同时留出一个步骤开放。
State——行为随所处的模式而变
红绿灯——绿、黄、红——每种颜色决定此刻发生什么,以及接下来是哪种颜色。同一盏灯,每种状态下行为不同。
State(状态)模式让对象在内部状态变化时改变自己的行为。一团纠缠的 if 和 switch 变成清晰的状态与转换——一台小小的状态机。它把一堆乱糟糟的标志位变成一台整洁的小机器。
- 问题
- 行为取决于某个模式,代码里塞满了标志位和
switch。 - 思路
- 每个状态是自己的对象,带着自己的规则和转换。
- 例子
一份订单在
pending、paid、shipped或cancelled下行为各不相同;每个状态只知道自己的规则以及自己能变成什么。- 代价
- 许多小类;只有两个状态时,一个普通的
if就够了。 - 何时使用
- 状态很多,而且状态之间的转换很重要。
- 关联
- 和 Strategy 形状相同,但这里是对象在存活过程中自己切换状态。
Iterator——逐个走过集合,而不看它的内部
电视遥控器的上一个和下一个——你在频道间切换,却不知道它们是怎么存放的。
**Iterator(迭代器)**提供一种统一的方式来遍历集合,而不暴露它是怎么构成的——数组、树或链表。这个模式太有用了,语言通常已经内置了它——for..of、生成器、__iter__。这是模式可以「毕业」成语言特性的最清楚的标志。
- 问题
- 遍历代码依赖于集合是怎么构成的。
- 思路
- 无论里面是什么,都用同一种方式取「下一个元素」。
- 例子
for (item of collection)对列表或自定义结构的工作方式都一样。- 代价
- 接近于零——通常语言里已经内置了。
- 何时使用
- 总是——但要通过内置工具,而不是手写。
- 关联
- 遍历用 Composite 建起来的树。
Template Method——固定的食谱,留一个空白待填
一份每次都一样的食谱——除了第四步「按你的口味加香料」。骨架是固定的;有一步归你。
Template Method(模板方法)在基类里定义一项操作的骨架,并允许重写其中特定的步骤,而不改变整体形状。当许多流程形状相同、只在一两处不同时很有用。
- 问题
- 几个形状相同的流程几乎完全互相复制。
- 思路
- 骨架放在基类里;会变化的步骤放在子类里。
- 例子
一个依次打开、读取、解析(这一步由你填)、保存的导入器;CSV 和 JSON 导入器只提供解析这一步。
- 代价
- 它依赖继承;骨架日后很难改。
- 何时使用
- 形状是共享的,差异只在一两处。
- 关联
- Strategy 用组合而不是继承来解决类似的问题。
Mediator、Visitor、Memento——其余的,一口气讲完
工具箱里有些工具你每月才用一次,而不是每天——但你仍然应该知道它们存在。
还有几个模式偶尔会派上用场。Mediator——一座控制塔,让对象通过一个枢纽对话,而不是一团直接的连线。Visitor——在不修改结构中各个类的前提下,为它增加新操作。Memento——捕获对象的状态,以便日后恢复。认得这些名字;只有当确切的问题出现时才去用它们。
- 是什么
- 一个枢纽,对象通过它对话,而不是彼此直接连接。
- 画面
- 一座控制塔:飞机和它对话,而不是彼此对话。
- 何时使用
- 对象之间的直接连接已经多得过头。
- 是什么
- 把对一个结构的新操作,挪到它自己的对象里。
- 例子
- 导出文档里的每个图形——而不修改图形类。
- 何时使用
- 结构是稳定的,但针对它的操作一直在增加。
- 是什么
- 对象的一个已保存状态,你可以回到它。
- 例子
- 游戏里的存档点,编辑器里的撤销快照。
- 关联
- 和 Command 一起,就有了撤销。
当一个模式变成了语言关键字,就别再手写这个模式,直接用关键字。
这份目录凝结了数十年来之不易的智慧——同时也是它所处时代的产物。像资深工程师那样使用它,而不是当作检查清单。
Language features——许多模式如今已经内置
过去我们靠地标指路;现在手机来导航。这门技能并没有消失——它被工具吸收了。
Iterator 就是 for..of。Strategy 常常是传进来的一个函数。Decorator 和 Observer 在现代框架里是一等公民。当语言白送你那个形状时,手写完整的模式就只是仪式(样板代码,boilerplate)。思想仍然重要;样板代码通常不重要。
Pattern-itis——一种真实的病
一个把整个调料架都倒进一道菜里的厨子,不会得到更丰富的一餐——只会得到一盘没法吃的东西。
**Pattern-itis(模式病)是典型的过度设计(over-engineering)**坏味道:为了显得高明而去拿模式。而现在更糟——AI 助手会很乐意为本该三行搞定的事生成一个「多层 Abstract Factory」。更简单的代码几乎总是更资深的选择。
- 是什么
- 为模式而模式,而不是为了缓解某种疼痛。
- 症状
为一个产品建的抽象工厂,为一个永不变化的算法建的 Strategy,围绕一个
if的五层间接。- 为什么现在更糟
- AI 助手生成这些结构既积极又快。
- 解药
- 先写最简单的代码;疼痛出现了再用模式。
模式是词汇,不是法律
认识很多词并不会让人成为好作家。知道该用哪个词——以及何时保持沉默——才会。
这份目录真正的价值是共享的语言:「这里放个 Facade 吧」,对任何懂这个术语的人都是一句话就明白,这在团队里价值很大。但目标始终是能解决问题的最简单的代码。模式是手段;说出一个模式的名字从来不是成就。
学会所有模式,是为了能认出它们。能少用就少用。
模式是答案。本事在于把它们对上一个真实的问题——并知道什么时候不用任何模式才是正确答案。
让模式从疼痛中长出来
你不会在找到螺栓之前就挑好扳手。先看到问题,再去拿合适的工具。
好的模式用法通常是一次重构,而不是一开始的蓝图:你先写简单的东西,感到某种具体的疼——这个 if 阶梯一直在变长,这个类对自己是怎么构造的知道得太多——然后缓解它的那个模式就显而易见了。一上来就硬套模式,就是在盖没人要的大教堂。
只在名字有帮助时才起名
闲聊时你不会说「我执行了一次撞墙配合」。但在球场上,和队友在一起,这个词能省下一整句话。
模式的回报是沟通和灵活性。如果起个名字能让下一个人更容易看懂代码,就大声用这个名字——放进类名、注释、设计讨论里。如果间接层什么也没换来,那就最朴素的代码获胜,不管它是不是模式。
模式之间是相互关联的
一门语言里的词不是孤立存在的:它们有近义词、有搭档、有亲戚。
Adapter、Decorator 和 Proxy 是同一种形状:包在对象外面的一层,只是目的不同。Strategy 和 State 形状相似。Command 和 Memento 合在一起给出撤销。Abstract Factory 由 Factory 构成。Composite 用 Iterator 遍历。Observer 是事件驱动代码的骨干。
- 它解决的是什么反复出现的问题? 用一句话说出来。
- 我现在就有这个问题吗,还是在猜测未来?
- 一个普通函数或对象够用吗? 先试试那个。
- 我的语言是不是已经白送了这个?
- 给它起名,会让下一个人更容易看懂代码吗——还是只是更花哨?
- 我是在朝它重构,还是一上来就硬塞进去?
- 你有一个只构造一样东西的工厂。
- 一个只有一种策略的 Strategy,而且永远不变。
- 接口和间接层比实际行为还多。
- 你加这个模式是为了显得资深,不是为了解决疼痛。
- 新读者需要一张 UML 图才能看懂三行逻辑。
- 这个模式去掉了一条不断变长的
if/switch阶梯。 - 你可以替换一个实现而不用动调用方。
- 同事一听名字就认出了那个形状。
- 间接层物有所值——你确实在用它带来的灵活性。
- 有这个模式的代码比没有它时更简单,而不只是更聪明。
速查表:全部 20 个模式与关键思想
基础概念
- Design Pattern设计模式
对代码中反复出现的问题的、有名字的解法。是词汇,不是库。
- Gang of FourGoF
《Design Patterns》的作者们,他们编目了 23 个模式。
- Indirection间接层
你和那个东西之间的一层。每个模式的代价。
- Pattern-itis模式病, over-engineering
为了展示而不是为了解决疼痛的模式。简单的代码几乎总是获胜。
创建型
- FactoryFactory Method, 工厂
按名字要一个对象;由工厂选择类。
- Abstract Factory抽象工厂
为一整族相配的对象准备的工厂。
- Builder建造者
一步一步造出复杂对象,最后
build()。- Singleton单例
人人共用恰好一个实例。小心:全局状态。
- Prototype原型
克隆一个现成的对象再稍作调整。
结构型
- Adapter适配器
一个转换器:让一个接口看起来像另一个。
- Decorator装饰器
包装层添加行为;接口保持不变。
- Facade外观
通向复杂子系统的一扇简单的门。
- Proxy代理
站在对象前面,控制访问。
- Composite组合
叶子和树共用一个接口。
行为型:对话
- Strategy策略
可替换的算法;常常只是一个函数。
- Observer观察者
订阅者会被告知变化。
- Command命令
把动作变成对象:排队、记录、撤销。
- Chain of Responsibility责任链, middleware
处理者排成一条链;在 Web 上就是中间件。
行为型:状态与步骤
- State状态
每个状态有自己的规则和转换。
- Iterator迭代器
不看内部就能遍历集合。通常就是
for..of。- Template Method模板方法
骨架是固定的;有一步归你。
- Mediator中介者
所有人都通过一个枢纽对话。
- Visitor访问者
不修改各个类就能加一个新操作。
- Memento备忘录
保存一个状态,日后回到它。
设计模式最好的用法,是读者从未察觉的那一种——因为它让代码显得理所当然。