速成课 · No. 04
它们当中大多数,你早已徒手重新发明过了。一个模式不过是个名字——好让一个团队用一个词说清原本要一整段话才说得明白的事,并在把某一步做砸之前就认出它。
只讲精髓 · 每个模式一幅画面 · 用例子代替 UML
设计模式是对一个在代码里一再出现的问题的、有名字的可复用答案。它是词汇,不是库——你不去安装它,你认出它。
模式是你早已会做的那一步的名字
球员不会在比赛中途现场发明「撞墙配合」。它有个名字,而说出这个名字,比把整套动作描述一遍要快。
大多数模式,本就是称职的开发者会徒手重新发明的东西。给它们起名,把一整段解释变成全队共享的一个词。当有人说「用一个 Decorator 把它包起来」,所有人立刻就看见了那个形状——而这正是模式的全部意义所在:一套共享的词汇,描述那些你无论如何都会做的步骤。
模式是被发现的,不是被发明的
没有人发明了拱。工匠们一次次发现它是跨越一道空隙最结实的办法——于是它有了名字。
Gang of Four 并没有设计出 23 个聪明的把戏;他们是把好代码里一再重现的解法编了目。所以模式不是一个要去命中的目标——它是当问题召唤它时,你自然而然会抵达的形状。如果你在硬塞一个模式进去,那你多半还没遇到那个问题。
三个家族,三个问题
一间厨房有对应三种活儿的工具:做菜、摆盘、上菜。模式也照同样的方式分家。
创建型模式关乎对象怎么诞生——由谁来造,怎么造。结构型模式关乎对象怎么拼成更大的形状。行为型模式关乎对象怎么对话、怎么分派职责。几乎每一个经典模式,都是这三个答案之一。
你不需要的模式,只是复杂度
口袋里揣一把瑞士军刀很顺手。把那二十个工具全拴到你家大门上,就不是了。
每一个模式都添了一层间接——在你和那个东西之间多一层。当它换来的灵活性你真的会用到时,这层是值的;当用不到时,它就是纯粹的成本。常见的错误不是不懂模式;而是在问题出现之前就伸手去拿一个。
模式是一次对话的捷径。用它来被人听懂,而不是用它来显得聪明。
对象怎么活过来——而不必把 new 撒得到处都是,也不必把你的代码焊死在具体类上。
Factory:按名字要一个东西,而不是按配方
一台咖啡机:你按下「拿铁」就得到一杯。你不必自己磨豆、打奶、倒杯——你也不需要知道它是怎么做出来的。
一个 factory 把一个对象递给你,而无需你直接调用它的构造器,于是调用方的代码从不依赖那个具体类。例子:createPaymentProvider("stripe") 返回某个满足 PaymentProvider interface 的东西——要换成 PayPal,只改这个 factory,而不是每一处调用点。它换来的自由是:改变被造出来的是什么,而不必去碰那个发出请求的人。
Abstract Factory:一次性给你一整套配套的家族
买一套家具——「维多利亚式」或「现代式」——每一件都以同一种风格抵达,没有一把不搭调的椅子。
当对象必须成套地、风格一致地出现时,一个 abstract factory 把整套造出来,好让它们彼此契合。例子:一套 UI 工具库,从同一个 factory 里为 macOS 和 Windows 各自产出配套的按钮、菜单和对话框。很强大——但它是一台重型机器,只有当那些家族真实存在时才值得。
Builder:一步步把一个复杂对象拼出来
点一个定制汉堡——面包、肉饼、不要洋葱、加一份芝士——一次一个选择,然后说一句「好了」。
当一个对象有许多可选的部件时,一个 builder 让你一块块地设置它们,而不必面对一个塞了十二个参数的怪物构造器。例子:QueryBuilder().select("name").where("age > 18").limit(10).build()。它用一条可读的链,换掉一个让人摸不着头脑的构造器。
Singleton:恰好一个,所有人共享
一个国家只有一位总统。每个提到「总统」的人,说的都是同一个人——不会有两个。
一个 singleton 保证只有一个实例,并配一个全局访问点。例子:一个所有人都从中读取的配置对象或 logger。但要小心拿捏:singleton 是披了件好外套的全局状态,而全局状态让测试和推理都变难。常常,一个你显式传进去的单一实例,胜过一个经由全局够到的实例。
Prototype:复制一个填好的东西,而不是从头造
影印一张填好的表格,比从一张空白表格重新填一遍要快。
当从头造一个对象既贵又麻烦时,就克隆一个现有的再做微调。例子:复制一个配置好的游戏敌人,或一个带样式的文档节点,而不是每次都重建它的那套设置。当对象造起来昂贵、又大体相似时很顺手。
把
new藏起来。知道一个对象怎么造的地方越少,你改它就越自由。
对象怎么组合成更大的结构——把不兼容的部件拼到一起,并在不重写的前提下添上新能力。
Adapter:两个 interface 之间的一只转接头
你在国外用的笔记本充电器——设备没问题,插座没问题;你只是需要它们之间那只小小的转接头。
一个 adapter 把一个 interface 包起来,让它看上去像另一个,从而让两个本不为合作而设计的东西能一起工作。例子:把一个第三方支付 SDK 包起来,让它满足你自己的 PaymentProvider interface——你的代码从不看见那个供应商的形状。它正是你如何阻止别人的 API 渗进你整个代码库的办法。
Decorator:加配料,而不改动那张披萨
一张原味披萨,然后加芝士,然后加蘑菇——每一层都裹住上一层、添上点什么,而它仍然是一张披萨。
一个 decorator 把一个对象包起来以添加行为,而不必改动原物、也不必继承它。例子:把一个数据流包起来加上压缩,再加上加密——每一个都是你能叠加或撤掉的一层。它让你以谁都不必预先设想到的组合,添上各种功能。
Facade:在一团乱的后台办公室前,立一个简单的前台
一位酒店礼宾——你说一句「我要一辆出租车和一个晚餐预订」,他就替你料理了柜台后面那一团电话往来。
一个 facade 为一个复杂的子系统提供一个简单的、单一的入口,把它那些活动的部件藏起来。例子:一个 VideoConverter.convert(file, "mp4"),在底下悄悄调度着编解码器、缓冲区和音频流。它把一个庞大的表面,收缩成大多数调用方唯一需要的那一扇门。
Proxy:一个控制访问的替身
一位明星的助理——你不直接够到那位明星;助理替他筛选、安排,有时还替他答话。
一个 proxy 站在一个对象前面以控制对它的访问——为了延迟加载、权限、缓存,或与某个远端的东西对话。例子:一个图片 proxy,直到图片被显示时才去加载真正的文件;或一个访问 proxy,在转发调用之前先检查权限。同一个 interface,前面多了一道闸。
Composite:把一棵树和一片叶子同等对待
一个文件夹里装着文件,也装着别的文件夹。「算出大小」这件事,无论你问的是一个文件还是整棵树,做法都一样。
一个 composite 让你把单个对象和成组的对象同等对待,于是一个枝杈和一片叶子共享同一个 interface。例子:一个 UI,其中一个按钮和一整面板的按钮都回应 render() 和 getSize()。它把递归结构,变成你无需对每一层都特殊处理就能应付的东西。
组合优于继承:在运行时把行为咔哒拼到一起,而不是把它冻死进一棵类的树里。
对象怎么分担活儿、怎么彼此对话——好让职责被干净地切开,并让对的代码在对的时刻运行。
Strategy:换掉算法,留住调用方
一个地图应用:同一段行程,但你挑「最快」「最短」还是「避开收费」,它就规划出不同的路线。你选策略;应用只管照着走。
Strategy 让一个算法在运行时可互换,于是调用方保持不变,而方法各异。例子:一个结账流程接收一个 PricingStrategy——普通、会员或节日促销——而不是一道越长越长的 if 阶梯。在当代语言里,这往往只是传进一个函数而非一整个类——而这正是要点:模式是那个想法,不是那套仪式。
Observer:订阅一下,它一变就有人告诉你
一份订阅刊物——你订一次,之后每一期新刊都送到你手上。出版方不会挨个给读者打电话;它只是把刊物发给名单。
Observer 让一些对象去订阅另一个对象,并在它变化时收到通知,而它无需知道它们是谁。例子:一个表格单元格,刷新所有依赖它的图表;UI 框架对状态作出反应;到处都是的 pub/sub。它是事件驱动与响应式代码的脊梁。
Command:把一个请求包成一个对象
一张餐厅点单票——「5 号桌,两杯咖啡」这个请求,变成一张你能排队、能重排、能存档、甚至能撕掉的单子。
Command 把一个动作变成一个独立的对象,于是你能把它排队、记录、调度,或撤销。例子:一个编辑器里的每一次编辑都是一个 command——而这恰恰是让撤销/重做和宏成为可能的东西;任务队列就是一个个等着轮到自己的 command。它正是一个动作如何变成你能存下来、能重放的东西的办法。
Chain of Responsibility:一路往下传,直到有人接手
客服的分层——前台先试,升级给专员,再升级给经理,直到有人真的能解决它。
一个请求沿着一条处理者的链行进;每一个要么处理它,要么把它往下传。例子:web 中间件——先鉴权,再日志,再限流,再到路由——每一环要么处理自己那一块,要么转发出去。它把发送方,和最终干活的那个人解耦开。
好的行为型模式,让两块代码协作,而不必其中任何一块把另一块硬接死。
一个对象的行为怎么随它的状态而变,你怎么一步步走过集合,以及你怎么定好一副骨架却留一步空着。
State:行为随你所处的模式而变
一盏红绿灯——绿、黄、红——每一种颜色都决定了此刻发生什么、以及下一个该是哪种颜色。同一盏灯,每种状态下行为不同。
State 模式让一个对象随它内部状态的改变而改变行为,用清晰的状态与转换,替掉一团 if 和 switch。例子:一个订单,在 pending、paid、shipped 或 cancelled 时各有各的行为,每个状态只知道自己的规则、以及它能变成什么。它把一堆乱糟糟的标志位,变成一台齐整的小机器。
Iterator:走过一个集合,而不见它的内里
一个电视遥控器的「下一个」和「上一个」——你在频道间移动,却不知道它们是怎么存的。
Iterator 给出一种统一的方式去走过一个集合,而不暴露它是怎么搭起来的——数组、树,还是链表。例子:for (item of collection) 在一个列表和一个自定义结构上做法相同。这一个有用到语言通常都把它内建了——for..of、生成器、__iter__——这是一个模式能晋级成一项语言特性最清楚的迹象。
Template Method:一份固定的配方,留一处空白要填
一份每次都一模一样的配方——除了第四步「加你自己选的香料」。骨架是固定的;有一步是你的。
Template Method 定义一个操作的骨架,并让具体的步骤可以被重写,而不改动整体的形状。例子:一个基础的数据导入器,它打开、读取、解析(这一步你来填)、保存——其中 CSV 和 JSON 导入器只提供那个解析步骤。当许多流程共享一个形状、却在一两处各异时很有用。
其余的,一口气讲完
一个工具箱里有些工具你一个月才拿一次,而不是每天——但你仍该知道它们存在。
还有几个偶尔配得上一席之地:Mediator(一座控制塔,好让对象通过一个枢纽对话,而不是一团直连的乱线)、Visitor(给一个结构添上新操作,而不编辑它的那些类)、Memento(捕捉一个对象的状态,好让你日后能恢复它——存档点、撤销快照)。记住这些名字;只有当那个确切的问题出现时再去拿它们。
当一个模式变成了一个语言关键字,就别再手写这个模式,用那个关键字。
这套目录是几十年里千辛万苦挣来的智慧——同时也是它那个时代的产物。像一个资深工程师那样去用它,而不是当成一张清单。
许多模式如今只是语言特性
我们曾经靠地标来指路;如今手机替你导航。那门本事没有消失——它被吸收进了工具里。
Iterator 就是 for..of。Strategy 往往就是传进去的一个函数。Decorator 和 Observer 在当代框架里是头等公民。当语言免费把那个形状递给你时,手写整套模式只是仪式。那个想法仍然重要;那些样板代码通常不重要。
模式癖是一种真实存在的病
一个把整架香料全倒进一道菜里的厨子,得到的不是一顿更丰盛的饭——而是一顿没法下咽的饭。
那个经典的过度工程的气味,就是为了显得高深而伸手去拿模式:为一个产品搞一个 abstract factory,为一个永不变化的算法搞一个 Strategy,在一个 if 周围裹上五层间接。而现在更糟了——一个 AI 助手会乐呵呵地为本该三行的东西生成一个「多层 Abstract Factory」。更简单的代码,几乎总是那个更资深的选择。
模式是一套词汇,不是一条法律
认得很多词,并不让你成为一个好作者。知道用哪个词——以及何时该闭嘴——才会。
这套目录真正的价值在于共享的语言:「我们在这儿放一个 Facade 吧」,对任何懂这个词的人都立刻就传达到了,而这在一个团队里很值钱。但目标永远是那个解决问题的、最简单的代码。模式是一个手段;给一个模式命名,从来不是那项成就本身。
把所有模式都学一遍,好让你能认出它们。能少用就少用。
模式是答案。本事在于把它们对上一个真实的问题——并知道何时「不用任何模式」才是对的答案。
让模式从那个痛里浮现出来
你不会在找到那颗螺栓之前就先挑一把扳手。你先看见问题,再去拿那个对得上它的工具。
用得好的模式,通常是一次重构,而不是一张起手的蓝图:你先写那个简单的东西,感到一个具体的痛——这道 if 阶梯越长越长,这个类对自己怎么被造出来知道得太多了——然后那个能缓解它的模式就显而易见了。在前头硬塞模式,就是你怎么盖出一座座没人要的大教堂。
只在名字有帮助时才说出它
你不会在闲聊里说「我刚完成了一次撞墙配合」。但在球场上,跟队友讲,这个词省下了一整句话。
一个模式的回报在于沟通和灵活。如果给它命名能让代码对下一个人更清楚,那就把这个名字说出来——放进一个类名、一句注释、一次设计讨论里。如果那层间接什么也没换来,那么最朴素的代码就赢,有没有模式都一样。
- 它解决的是哪个反复出现的问题? 用一句话把它说出来。 - 我现在有那个问题吗, 还是我在对未来瞎猜? - 一个朴素的函数或对象行不行? 先试那个。 - 我的语言是不是已经免费 给了我这个? - 给它命名会让代码对下一个人更清楚——还是只是更花哨? - 我是在朝它重构,还是在前头硬塞它?
- 你有一个只造一个东西的 factory。 - 一个只有一种策略的 Strategy, 而它从不变化。 - interface 和间接,比真正的行为还多。 - 你加上这个模式,是为了显得资深,而不是为了解决一个痛。
- 一个新读者需要一张 UML 图,才跟得上三行逻辑。
- 这个模式去掉了一道越长越长的
if/switch阶梯。 - 你能换掉一个 实现,而不必去碰那些调用方。 - 一个队友从它的名字一眼就认出了那个形状。 - 那层间接挣回了它的本钱——你真的用到了那份灵活。 - 代码有了这个模式,比没有它更简单,而不只是更聪明。
一个设计模式最好的用法,是读者从不察觉的那一种——因为它让代码读起来理所当然。