fedorthinks

速成课 · No. 04

它们当中大多数,你早已徒手重新发明过了。一个模式不过是个名字——好让一个团队用一个词说清原本要一整段话才说得明白的事,并在把某一步做砸之前就认出它。

20 个模式 · 每个配图示 · 结尾有速查表

§ 01

设计模式是对一个在代码里一再出现的问题的、有名字的可复用答案。它是词汇,不是库——你不去安装它,你认出它。

Design Pattern——给你早已会做的那一步起的名字

球员不会在比赛中途现场发明「撞墙配合」。它有个名字,而说出这个名字,比把整套动作描述一遍要快。

设计模式是称职的开发者无论如何都会徒手重新发明的一步。给它起个名字,就把一段解释变成了全队共享的一个词。当有人说「用 Decorator 包一层」,所有人立刻看到那个形状——这就是模式全部的意义:一套共享的词汇,用来指称你本来就会做的那些步骤。

ConceptDesign Pattern· 设计模式
是什么
对代码中反复出现的问题的、有名字的解法。
不是什么
不是库:你不去安装它,你认出它,然后自己写。
尺度
活在单个模块的代码里——不同于架构模式,后者决定整个系统的平面图。
为什么
共享语言:一个词代替一段话。

Gang of Four——模式是被发现的,不是被发明的

没有人发明拱。建造者们一再发现它是跨越空隙最牢固的办法——于是它有了名字。

Gang of Four(GoF,四人组)是《Design Patterns》一书的四位作者。他们并没有设计出 23 个巧妙的花招;他们是把好代码里一再出现的解法编成了目录。所以模式不是要去命中的目标——它是问题需要时你自然而然走到的形状。如果你是硬塞进去的,多半是你还没遇到那个问题。

地图:三个家族,三个问题

厨房里有三类活的工具:做菜、装盘、上菜。模式也是这样划分的。

创建型模式讲的是对象怎么诞生——谁来构造它们,怎么构造。结构型模式讲的是对象怎么拼成更大的形状。行为型模式讲的是对象怎么对话、怎么分担责任。几乎每个经典模式都是这三个答案之一。

Creationalhow objects are bornFactoryAbstract FactoryBuilderSingletonPrototypeStructuralhow objects fit togetherAdapterDecoratorFacadeProxyCompositeBehaviouralhow objects talk and share workStrategyObserverCommandChain of ResponsibilityStateIteratorTemplate MethodMediatorVisitorMemento
三个家族和本课的 20 个模式——第 2–5 章依此展开。

Indirection——你用不着的模式只是复杂度

口袋里的瑞士军刀很顺手。把二十件工具全拧到你家大门上,就不然了。

每个模式都会增加间接层(indirection)——你和那个东西之间多出一层。当它换来的灵活性你真会用到时,这值得;不会用到时,就是纯粹的代价。常见的错误不是不懂模式,而是在问题还不存在时就伸手去拿一个。

怎么读每个模式

护照:每本的栏目都一样,所以很容易比较。

从这里开始,每个模式都有相同的几个部分:它的英文名称(那就是术语——记住它)、一幅取自日常生活的画面、一张图示,以及一张带栏目的卡片:问题(哪里疼)、思路(怎么解决)、例子、代价(你付出什么)、何时使用,以及关联模式。结尾有一张把所有名称放在一页上的速查表。

模式是对话的捷径。用它是为了被人听懂,不是为了显得聪明。

§ 02

对象如何诞生——而不必把 new 撒得到处都是,也不把你的代码焊死在具体的类上。

Factory——按名字要东西,而不是按配方

咖啡机:你按下「拿铁」,就得到一杯。你不用自己研磨、打奶泡、倒——也不需要知道它是怎么做的。

Factory(工厂)把对象交给你,而不用你直接调用它的构造函数,所以调用方的代码从不依赖具体的类。它只认识接口。它换来的是自由:可以改变被构造的东西,而不必去动提出请求的那一方。

Clientasks by namecreate("stripe")Factorypicks the classStripeProviderPayPalProviderFakeProviderthe caller knows only the PaymentProvider interface — never the concrete class
客户端按名字请求;工厂选择类;客户端只看得到 PaymentProvider 接口。
PatternFactory· Factory Method, 工厂
问题
new StripeProvider() 散落在代码各处——被焊死在一个类上。
思路
向工厂要对象;由它决定构造哪个类。
例子

createPaymentProvider("stripe") 返回一个 PaymentProvider。换成 PayPal,只需改工厂,不用改每一处调用。

代价
多一层。一个只构造一样东西的工厂是坏味道。
何时使用
有好几种实现,或者将来会有。
关联
Abstract Factory——同一个思路,用于一整族对象。

Abstract Factory——一次造出一整套相配的家族

买一套家具——「维多利亚风」或「现代风」——每一件都是同一种风格,不会有不配套的椅子。

当对象必须成族地保持一致时,**Abstract Factory(抽象工厂)**一次造出整套,让各部分彼此契合。很强大——但它是重型机械,只有当这些家族是真实存在的时候才值得。

macOS factoryButtonmacMenumacDialogmacWindows factoryButtonwinMenuwinDialogwinpick ONE familyone factory = one matching set; never a mac Button next to a Windows Menu
一个工厂,一套相配的东西:同一风格的按钮、菜单和对话框。
PatternAbstract Factory· 抽象工厂
问题
对象之间必须相配,而且很容易搞混。
思路
一个工厂以一种风格造出整套对象。
例子

一个 UI 工具包,用同一个工厂为 macOS 或 Windows 造出相配的按钮、菜单和对话框。

代价
重型机械:大量的接口和类。
何时使用
只有当家族是真实的,而且不止一个时。
关联
由 Factory 构成。「只服务一个产品的抽象工厂」是典型的过度设计。

Builder——一步一步组装复杂对象

点一份定制汉堡——面包、肉饼、不要洋葱、加双份奶酪——一次选一样,最后说「好了」。

当一个对象有许多可选部分时,**Builder(建造者)**让你一块一块地设置,而不是用一个带十二个参数的怪物构造函数。最后调用 build(),拿到成品。它把一个令人费解的构造函数换成了一条可读的链。

set one optional part at a timeselect(name)where(age>18)limit(10)build()Queryready, valid objectnew Query(a, b, c, … 12 args)which argument is which?instead ofa readable chain replaces a monstrous constructor
一串步骤 → build() → 一个成品对象。而不是一个你分不清哪个参数是哪个的构造函数。
PatternBuilder· 建造者
问题
一个十二个参数的构造函数,其中一半是可选的。
思路
一次设置一个部分,最后 build()。
例子

QueryBuilder().select("name").where("age > 18").limit(10).build()。

代价
真正的类旁边多出一个类。
何时使用
对象有许多可选部分。
关联

Factory 决定构造_什么_;Builder 决定_怎样_组装复杂的东西。

Singleton——有且只有一个,人人共用

一个国家有一位总统。所有提到「总统」的人指的都是同一个人——不会有两个。

Singleton(单例)保证只有一个实例和一个全局访问点。但要小心:单例是穿着漂亮外套的全局状态,而全局状态会让测试和推理变难。

BillingAuthReportsAPIgetInstance()Configthe one instanceeveryone reaches the same global — handy, but hard to test
每个模块都拿到同一个实例——方便,但这是全局状态。
PatternSingleton· 单例
问题
你需要恰好一个共享实例。
思路
由类本身保证一个实例和一个访问点。
例子
所有人都从中读取的配置对象或日志器。
代价
全局状态:测试和推理都变得更难。
何时使用
很少。一个你显式传入的单一实例,往往胜过通过全局访问到的那个。
关联
和 Factory 一样属于创建型,但关心的是「几个」,而不是「哪一个」。

Prototype——复制一份填好的,而不是从头新建

复印一张已经填好的表,比从空白表格从头填起要快。

当从零开始造一个对象既昂贵又琐碎时,Prototype(原型)让你把现有的对象克隆一份再稍作调整。适用于构造成本高、而且大多彼此相似的对象。

Enemyconfigured: hp, skin, AIclone() + tweakcopy #1tweak: colourcopy #2tweak: speedcopy #3as ischeaper thanbuilding from zerocopy a filled-in object instead of rebuilding its setup
一个配置好的对象被克隆并略加调整——而不是重新构造。
PatternPrototype· 原型
问题
从零构造这个对象既慢又琐碎。
思路
拿一个现成的对象,复制它,调整差异的部分。
例子
复制一个已配置好的游戏敌人,或一个带样式的文档节点。
代价
复制需要小心:哪些被复制,哪些仍是共享的。
何时使用
对象构造成本高,而且大多相似。
关联
Factory 的一个替代方案:不是「构造」,而是「复制」。

把 new 藏起来。知道对象怎么构造的地方越少,你改它就越自由。

§ 03

对象如何组合成更大的结构——把不兼容的部分拼在一起,不重写就能增加能力。

Adapter——两个接口之间的转换插头

你的笔记本充电器到了国外——设备没问题,插座也没问题;只差中间那个小转换器。

**Adapter(适配器)**包装一个接口,让它看起来像另一个,使两个本来不是为了合作而设计的东西能一起工作。这是你阻止别人的 API 渗透到整个代码库的办法。

Your codeexpects your interfacepay()Adaptertranslates callscharge()Vendor SDKits own shapeyour code never sees the vendor's shape
你的代码调用 pay();适配器把它翻译成供应商的 charge()。
PatternAdapter· 适配器, wrapper
问题
别人的接口和你的对不上,而且在你的代码里四处蔓延。
思路
一个包装层,把调用从一个接口翻译到另一个接口。
例子
把第三方支付 SDK 包装起来,使它满足你自己的 PaymentProvider。
代价
供应商的 API 变化时,又多一层要更新。
何时使用
接入第三方代码或遗留代码。
关联
像 Decorator 和 Proxy 一样包装对象,但它会改变接口。

Decorator——加配料,而不改披萨本身

一张普通披萨,再加奶酪,再加蘑菇——每一层都包住上一层并添加点什么,它仍然是披萨。

**Decorator(装饰器)**包装一个对象来添加行为,既不修改原对象,也不去继承它。接口保持不变,所以各层可以叠加。它让你以没人需要事先预料的组合方式添加功能。

Encrypt decoratorCompress decoratorFileStreamthe originalwrite()each layer wraps the last, adds one thing, keeps the same interface
一个数据流被压缩包住,压缩又被加密包住。从外面看,它仍然是一个数据流。
PatternDecorator· 装饰器, wrapper
问题
你需要任意组合的附加功能;为每种组合写一个子类无法扩展。
思路
包住对象,添加一种行为,保持接口不变。
例子
数据流 → 压缩 → 加密;每一层都可以叠加或移除。
代价
许多小包装;一叠层层包裹更难调试。
何时使用
能力需要在运行时组合。
关联

和 Proxy 形状相同,但目的是添加,不是控制。组合优于继承的教科书式案例。

Facade——杂乱后台前的一个简单前台

酒店礼宾员——你说「我需要一辆出租车和一个晚餐预订」,柜台后面那一团电话由他们去处理。

Facade(外观)为复杂的子系统提供一个简单、单一的入口,隐藏它那些活动部件。它把一大片接口面缩成大多数调用方只需要的那一扇门。

ClientVideoConverterconvert(file, mp4)one doorfor most callerssubsystemCodecsBuffersAudio streams
一个 convert() 调用——背后是编解码器、缓冲区和音频流。
PatternFacade· 外观
问题
做一件简单的事,却要了解十几个子系统类。
思路
一扇简单的门,统筹调度整个子系统。
例子

VideoConverter.convert(file, "mp4") 在背后默默调度编解码器、缓冲区和音频流。

代价
复杂度并没有消失——只是被藏了起来。精细控制仍然要绕过外观去做。
何时使用
大多数调用方只需要一两种简单场景。
关联
Adapter 改变一个东西的接口;Facade 简化整个子系统。

Proxy——控制访问的替身

名人的助理——你不能直接找到明星;助理负责筛选、安排日程,有时还替他们回答。

Proxy(代理)站在对象前面控制对它的访问——用于懒加载(lazy loading)、权限、缓存,或与远程的东西通信。同样的接口,前面多了一道门。

ClientProxysame interfaceonly if neededRealImageheavy, loaded latelazy load · access checkcache · remote call
客户端把代理当作真实对象来对话;代理决定何时——以及是否——把调用转发出去。
PatternProxy· 代理, stand-in
问题
对象加载昂贵、需要权限,或者位于另一台机器上。
思路
一个接口相同的替身,站在真实对象前面。
例子

一个图片代理,在真正展示之前不加载真实文件;一个访问代理,在转发调用之前先检查权限。

代价
每次调用多一跳;延迟对调用方是隐藏的。
何时使用
懒加载、权限、缓存、远程对象。
关联
和 Decorator 形状相同,目的不同——访问控制。

Composite——把一棵树和一片叶子同等对待

一个文件夹里有文件和其他文件夹。「计算大小」无论问一个文件还是整棵树,做法都一样。

**Composite(组合)**让你统一对待单个对象(叶子)和一组对象(组合体)——它们共用一个接口。它把递归结构变成了你无需在每一层都做特殊处理就能操作的东西。

Folderfile.txtFolderphoto.jpga.mdb.mdgetSize() and render() work the same on one leaf and on a whole tree
文件夹和文件响应同样的调用——遍历这棵树不需要特殊情况。
PatternComposite· 组合, 树
问题
处理一棵树,每一层都需要一个特殊情况。
思路
叶子和分支共用一个接口;分支只是去调用它的子节点。
例子
一个 UI,按钮和装满按钮的面板都能响应 render() 和 getSize()。
代价
共用的接口可能过于笼统:并非每个操作对叶子都有意义。
何时使用
数据天然就是一棵树:文件夹、UI、文档。
关联
用 Iterator 遍历;用 Visitor 添加新操作。

组合优于继承:在运行时把行为拼装起来,而不是把它冻结在一棵类继承树里。

§ 04

行为型模式的上半部分:对象如何分担工作、彼此对话——让责任划分干净,让正确的代码在正确的时刻运行。

Strategy——换掉算法,调用方不变

一个地图应用:同一段行程,你选「最快」「最短」或「避开收费路段」,它就规划出不同的路线。你选策略;应用只是照着走。

Strategy(策略)让算法在运行时可以互换,调用方保持不变而方法可以变化。在现代语言里,这往往只是传入一个函数,而不是一整个类——这正是重点:模式是那个思想,不是那套仪式。

Checkoutuses a strategyPricingStrategyinterface / functionRegularMemberHolidaySaleswap the algorithm at runtime; the caller never changes
结账流程调用一个 PricingStrategy;具体用哪种定价,在运行时决定。
PatternStrategy· 策略
问题
围绕同一算法的各种变体,if 阶梯越长越多。
思路
算法是接口背后一个可替换的对象(或函数)。
例子
一个接收 PricingStrategy 的结账流程:普通价、会员价或节日促销价。
代价
给一个永不变化的算法套上 Strategy 是累赘。
何时使用
有两个或更多变体,在运行时选择。
关联
和 State 形状相同。在现代代码里,常常只是一个函数。

Observer——订阅,变了就通知你

一份新闻简报——你订阅一次,每一期新刊都会送到你手上。出版方不会给每位读者打电话;它只是发给名单。

在 Observer(观察者)中,订阅者向主题注册,主题一变化就通知它们——而主题并不知道它们是谁。它是事件驱动和响应式代码的骨干。

notify()Subjectspreadsheet cellChartPivot tableSummarythe subject keeps a list of subscribers — it doesn't know who they are
单元格变了 → notify() → 每个依赖它的图表和表格自己更新。
PatternObserver· 观察者, pub/sub
问题
一个对象必须把变化告诉许多其他对象,却不能按名字认识它们。
思路
一份订阅者名单 + 每次变化时 notify()。
例子

电子表格的一个单元格刷新所有依赖它的图表;UI 框架对状态做出反应;到处都是 pub/sub。

代价
通知链很难追踪;被遗忘的订阅会永远存活。
何时使用
若干相互独立的部分要对同一个变化做出反应。
关联

事件驱动代码的基础——和 Event-driven 架构是一回事,只不过发生在一个程序内部。

Command——把请求包装成对象

餐厅的点单——「5 号桌,两杯咖啡」这个请求变成了一张单子,你可以排队、重排、记录,甚至撕掉。

Command(命令)把一个动作变成一个独立的对象,于是你可以把它排队、记录、定时,或者撤销。这是让一个动作变成可以存储、可以重放的东西的办法。

Editoruser actioncommand historyTypeTextdo() · undo()Bolddo() · undo()Deletedo() · undo()executeDocumentreceiveran action becomes an object: queue it, log it, undo it, replay it
每次编辑都是一个带 do() 和 undo() 的对象;命令历史带来撤销/重做和宏。
PatternCommand· 命令
问题
一个动作需要被延迟、重复、记录或撤销。
思路
把动作打包进一个对象,里面带着执行它所需的一切。
例子

编辑器里的每次编辑都是一个命令——因此有了撤销/重做和宏。作业队列就是等着轮到自己的命令。

代价
每个动作一个对象;每个命令的撤销都要单独想清楚。
何时使用
撤销/重做、队列、宏、动作日志。
关联
常与 Memento 搭配来实现撤销。

Chain of Responsibility——一路传下去,直到有人处理

客服的分级——前台先试,升级给专家,再升级给经理,直到有人真能解决。

在 Chain of Responsibility(责任链)里,请求沿着一条处理者链传递;每个处理者要么处理它,要么传给下一个。它让发送者与最终干活的人解耦。

middleware chainRequestAuthLoggingRate limitRoutehandles it401, stopeach link handles its part — or passes the request on
Web 中间件:认证 → 日志 → 限流 → 路由。任何一环都可以拦下请求。
PatternChain of Responsibility· 责任链, middleware
问题
发送者不应该知道究竟是谁来处理请求。
思路
一条处理者链:每个做自己那一份,或者传下去。
例子
Web 中间件:先认证,再日志,再限流,最后才是路由本身。
代价
各环的顺序很重要;请求可能走完整条链也没人处理。
何时使用
在主要工作之前,有若干相互独立的检查或步骤。
关联
在 Web 上你几乎总是以 middleware 的形式遇到它。

好的行为型模式让两个部分能够合作,而不让任何一方把另一方写死在自己里面。

§ 05

行为型模式的下半部分:对象的行为如何随状态变化,你如何逐个遍历集合,以及如何固定一副骨架、同时留出一个步骤开放。

State——行为随所处的模式而变

红绿灯——绿、黄、红——每种颜色决定此刻发生什么,以及接下来是哪种颜色。同一盏灯,每种状态下行为不同。

State(状态)模式让对象在内部状态变化时改变自己的行为。一团纠缠的 if 和 switch 变成清晰的状态与转换——一台小小的状态机。它把一堆乱糟糟的标志位变成一台整洁的小机器。

pendingpaidshippedcancelledpay()ship()cancel()cancel()each state knows only its own rules and what it can become
一份订单:待支付 → 已支付 → 已发货;只有在待支付或已支付时才能取消。
PatternState· 状态, state machine
问题
行为取决于某个模式,代码里塞满了标志位和 switch。
思路
每个状态是自己的对象,带着自己的规则和转换。
例子

一份订单在 pending、paid、shipped 或 cancelled 下行为各不相同;每个状态只知道自己的规则以及自己能变成什么。

代价
许多小类;只有两个状态时,一个普通的 if 就够了。
何时使用
状态很多,而且状态之间的转换很重要。
关联
和 Strategy 形状相同,但这里是对象在存活过程中自己切换状态。

Iterator——逐个走过集合,而不看它的内部

电视遥控器的上一个和下一个——你在频道间切换,却不知道它们是怎么存放的。

**Iterator(迭代器)**提供一种统一的方式来遍历集合,而不暴露它是怎么构成的——数组、树或链表。这个模式太有用了,语言通常已经内置了它——for..of、生成器、__iter__。这是模式可以「毕业」成语言特性的最清楚的标志。

ArrayTreeLinked listIteratornext() · hasNext()for..of loopsame code for allwalk any collection without seeing how it is stored
数组、树和链表都给出同样的 next()——循环不知道里面是什么。
PatternIterator· 迭代器
问题
遍历代码依赖于集合是怎么构成的。
思路
无论里面是什么,都用同一种方式取「下一个元素」。
例子
for (item of collection) 对列表或自定义结构的工作方式都一样。
代价
接近于零——通常语言里已经内置了。
何时使用
总是——但要通过内置工具,而不是手写。
关联
遍历用 Composite 建起来的树。

Template Method——固定的食谱,留一个空白待填

一份每次都一样的食谱——除了第四步「按你的口味加香料」。骨架是固定的;有一步归你。

Template Method(模板方法)在基类里定义一项操作的骨架,并允许重写其中特定的步骤,而不改变整体形状。当许多流程形状相同、只在一两处不同时很有用。

fixed skeleton in the base classopen()read()parse()you fill this insave()CSV parseJSON parsesubclasses supplyonly this one step
基础导入器依次打开、读取、解析、保存;子类只提供解析这一步。
PatternTemplate Method· 模板方法
问题
几个形状相同的流程几乎完全互相复制。
思路
骨架放在基类里;会变化的步骤放在子类里。
例子

一个依次打开、读取、解析(这一步由你填)、保存的导入器;CSV 和 JSON 导入器只提供解析这一步。

代价
它依赖继承;骨架日后很难改。
何时使用
形状是共享的,差异只在一两处。
关联
Strategy 用组合而不是继承来解决类似的问题。

Mediator、Visitor、Memento——其余的,一口气讲完

工具箱里有些工具你每月才用一次,而不是每天——但你仍然应该知道它们存在。

还有几个模式偶尔会派上用场。Mediator——一座控制塔,让对象通过一个枢纽对话,而不是一团直接的连线。Visitor——在不修改结构中各个类的前提下,为它增加新操作。Memento——捕获对象的状态,以便日后恢复。认得这些名字;只有当确切的问题出现时才去用它们。

MediatorVisitorMementoABCDHubone hub, not a tangleCircleSquareTextExportVisitornew operationclasses stay untouchedEditorSnapshotsaved statesaverestoreundo · save points
Mediator:一切经过枢纽。Visitor:新操作从外部遍历各个类。Memento:快照与恢复。
PatternMediator· 中介者
是什么
一个枢纽,对象通过它对话,而不是彼此直接连接。
画面
一座控制塔:飞机和它对话,而不是彼此对话。
何时使用
对象之间的直接连接已经多得过头。
PatternVisitor· 访问者
是什么
把对一个结构的新操作,挪到它自己的对象里。
例子
导出文档里的每个图形——而不修改图形类。
何时使用
结构是稳定的,但针对它的操作一直在增加。
PatternMemento· 备忘录, snapshot
是什么
对象的一个已保存状态,你可以回到它。
例子
游戏里的存档点,编辑器里的撤销快照。
关联
和 Command 一起,就有了撤销。

当一个模式变成了语言关键字,就别再手写这个模式,直接用关键字。

§ 06

这份目录凝结了数十年来之不易的智慧——同时也是它所处时代的产物。像资深工程师那样使用它,而不是当作检查清单。

Language features——许多模式如今已经内置

过去我们靠地标指路;现在手机来导航。这门技能并没有消失——它被工具吸收了。

Iterator 就是 for..of。Strategy 常常是传进来的一个函数。Decorator 和 Observer 在现代框架里是一等公民。当语言白送你那个形状时,手写完整的模式就只是仪式(样板代码,boilerplate)。思想仍然重要;样板代码通常不重要。

the patternwhat the language gives you nowIteratorfor..of · generators · __iter__Strategya function passed inDecoratorbuilt into frameworksObserverreactive state in UI frameworks
左边是目录里的模式;右边是语言或框架已经替你提供的东西。

Pattern-itis——一种真实的病

一个把整个调料架都倒进一道菜里的厨子,不会得到更丰富的一餐——只会得到一盘没法吃的东西。

**Pattern-itis(模式病)是典型的过度设计(over-engineering)**坏味道:为了显得高明而去拿模式。而现在更糟——AI 助手会很乐意为本该三行搞定的事生成一个「多层 Abstract Factory」。更简单的代码几乎总是更资深的选择。

Anti-patternPattern-itis· 模式病, over-engineering
是什么
为模式而模式,而不是为了缓解某种疼痛。
症状

为一个产品建的抽象工厂,为一个永不变化的算法建的 Strategy,围绕一个 if 的五层间接。

为什么现在更糟
AI 助手生成这些结构既积极又快。
解药
先写最简单的代码;疼痛出现了再用模式。

模式是词汇,不是法律

认识很多词并不会让人成为好作家。知道该用哪个词——以及何时保持沉默——才会。

这份目录真正的价值是共享的语言:「这里放个 Facade 吧」,对任何懂这个术语的人都是一句话就明白,这在团队里价值很大。但目标始终是能解决问题的最简单的代码。模式是手段;说出一个模式的名字从来不是成就。

学会所有模式,是为了能认出它们。能少用就少用。

§ 07

模式是答案。本事在于把它们对上一个真实的问题——并知道什么时候不用任何模式才是正确答案。

让模式从疼痛中长出来

你不会在找到螺栓之前就挑好扳手。先看到问题,再去拿合适的工具。

好的模式用法通常是一次重构,而不是一开始的蓝图:你先写简单的东西,感到某种具体的疼——这个 if 阶梯一直在变长,这个类对自己是怎么构造的知道得太多——然后缓解它的那个模式就显而易见了。一上来就硬套模式,就是在盖没人要的大教堂。

只在名字有帮助时才起名

闲聊时你不会说「我执行了一次撞墙配合」。但在球场上,和队友在一起,这个词能省下一整句话。

模式的回报是沟通和灵活性。如果起个名字能让下一个人更容易看懂代码,就大声用这个名字——放进类名、注释、设计讨论里。如果间接层什么也没换来,那就最朴素的代码获胜,不管它是不是模式。

模式之间是相互关联的

一门语言里的词不是孤立存在的:它们有近义词、有搭档、有亲戚。

Adapter、Decorator 和 Proxy 是同一种形状:包在对象外面的一层,只是目的不同。Strategy 和 State 形状相似。Command 和 Memento 合在一起给出撤销。Abstract Factory 由 Factory 构成。Composite 用 Iterator 遍历。Observer 是事件驱动代码的骨干。

all wrap one objectAdapterchanges the interfaceDecoratoradds behaviourProxycontrols accessStrategyStatesame shapeCommandMementoundoAbstract FactoryFactoryusesCompositeIteratorwalk the treeObserverEvent-driven codebackbone ofChain of Responsibility= web middleware
本课各模式之间的主要关联。
在伸手去拿模式之前
  • 它解决的是什么反复出现的问题? 用一句话说出来。
  • 我现在就有这个问题吗,还是在猜测未来?
  • 一个普通函数或对象够用吗? 先试试那个。
  • 我的语言是不是已经白送了这个?
  • 给它起名,会让下一个人更容易看懂代码吗——还是只是更花哨?
  • 我是在朝它重构,还是一上来就硬塞进去?
判断你是否过度设计的气味测试
  • 你有一个只构造一样东西的工厂。
  • 一个只有一种策略的 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备忘录

保存一个状态,日后回到它。

设计模式最好的用法,是读者从未察觉的那一种——因为它让代码显得理所当然。

速成课结束 · 7 章 · 20 个模式

接下来是源头:Gang of Four 的 Design Patterns,以及它那些更友善的当代同伴——Head First Design Patterns、refactoring.guru、Martin Fowler 的 Refactoring。但要像你学棋开局那样去读它们:不是为了背招法,而是为了当那个局面出现在你自己的棋盘上时,能认出它。