Chapter 05 · 辨析与判断
辨析与判断:把「读意图」放到真实场景里考一遍
04 兑现了策略 vs 状态的王牌辨析。前四章把 14 个模式逐个讲完,每个都问过「分离出什么、意图是什么」。这一章不再讲新模式——它把这些判断混在一起,强迫你在真实场景里选对那一个;再补上前四章刻意压后的一层:什么时候不该用模式。
本章你将建立的 schema
- 把跨章的辨析连起来:同一个「包一层 / 换行为」的动作,意图不同就是不同的模式。
- 一棵「先读意图、再定模式」的总决策树,把 14 个模式收进三个问题。
- 2026 年的判断层:哪些模式被语言特性吸收、哪些已成反模式、什么时候模式本身就是过度设计。
5.0这一章是考场,不是讲堂
前四章每个模式单独看都「读得很顺」。但起点的警告说过:顺,只在分不清两个模式时才被戳破。下面六个场景,每一个都把来自不同章的模式摆在一起,逼你做选择。规则:先合上前面的章节,自己给出答案和理由,再展开对照。
每道题的正确答案都不靠「记住哪个模式长什么样」,而靠同一句话——这里会变的到底是什么?意图是什么?
5.1跨章辨析:六个场景
一个游戏里的敌人 AI:平时巡逻,发现玩家后转入追击,血量过低转入逃跑。三种模式下的行动逻辑完全不同,而且这三种模式之间会互相切换。用策略还是状态?
展开答案(先写下你的判断)
状态。关键信号是「三种模式之间互相切换」,而且切换由 AI 当前所处的模式自己触发(追击中血量掉到阈值 → 自己转入逃跑)。04 章的判定法:可互换的对象之间会不会自切换?会 → 状态。
若误用策略,切换逻辑会被推到外部客户端,客户端被迫知道每种模式之间的全部转移条件——这正是状态模式要消除的东西。
要给一个第三方支付 SDK「包一层」。分别考虑三种需求:(a) SDK 的接口和你系统期望的不一致;(b) 想在每次调用前后加日志和重试,而且这些增强要能自由叠加;(c) 这个 SDK 初始化很贵,想推迟到真正调用时才创建连接。三种需求各自该用哪个模式?
展开答案(先写下你的判断)
同一个「包一层」的动作,三种意图 → 三个模式(03 章的 wrapper 四兄弟辨析):
(a) 接口不一致 → 适配器(意图:翻译接口,接口改变)。
(b) 叠加增强、可自由组合 → 装饰器(意图:加职责,接口相同但增强,由客户端控制叠几层)。
(c) 推迟昂贵的初始化 → 代理(意图:控制访问 / 懒加载,接口完全相同,代理自己管理真实对象的生命周期)。
读法永远是先问「接口变不变」,再问「意图是控访问还是加功能」。结构帮不上忙——三者都是「持有被包对象 + 委托」。
做一个跨平台 UI 库。需求 A:同一界面里的按钮、菜单、滚动条必须同属 macOS 风格或 Windows 风格,绝不能混搭。需求 B:要构造一个有十几个可选项(标题、图标、按钮组、回调…)的对话框,配置过程分好几步。A、B 各用哪个创建型模式?
展开答案(先写下你的判断)
A → 抽象工厂(02 章):意图是创建「一族相关产品」并保证它们同族。一致性之所以有保证,是因为整族控件都来自同一个工厂实例,混搭在结构上就不可能。
B → 建造者:意图是把「分步构造一个复杂对象」与它的表示分离,专治十几个可选参数的望远镜构造函数。
区分轴:A 关心的是「一族对象的一致性」,B 关心的是「单个对象的构造过程」。
一个通知系统有两条会各自独立增长的维度:消息级别(普通 / 加急 / 紧急,各自有不同的重试与降级逻辑)和发送渠道(短信 / 邮件 / 推送)。该用桥接还是策略?
一个表单页面:多个控件之间要联动——改了「国家」下拉框要刷新「城市」列表,勾了「同上」要把收货地址复制到账单地址,等等。控件之间的联动关系很多。用观察者还是中介者?
展开答案(先写下你的判断)
倾向中介者(04 章)。这里是「多个组件之间多对多的复杂联动」,中介者把所有联动逻辑收进一个中心对象,让控件之间互不直接认识。
观察者解决的是「一个 subject 状态变了,一对多地广播给一群订阅者」——单向、一对多。若这里只是「一个数据源变了通知多个视图」,那才是观察者。
判别:一对多的单向广播 → 观察者;多对多、要消除组件间的相互引用 → 中介者。
Code review 时看到一段代码:定义了 interface SortStrategy,只有一个实现 QuickSort,而且排序方式在可预见的将来不会再变。作者说「这样符合策略模式,更专业」。该保留还是该改?
展开答案(先写下你的判断)
该改回内联。策略的前提是真的存在多种可互换算法。只有一个实现、且不会增加,接口只是凭空多一层间接——读代码的人要多跳一次才看到真正发生了什么,却换不来任何灵活性。这是典型的过度设计。
正确节奏:先把排序直接写着,等第二种排序需求真的出现,再重构成策略。模式是对已出现或高度可预期变化的回应,不是预先铺的脚手架。5.2 详谈这层判断。
5.22026 年的判断层:什么时候不该用模式
前四章教的是「这个模式怎么回事」。这一节补上同样重要的另一半——这个模式今天还值不值得用。GoF 的 23 个模式定稿于 1994 年,模式本身没变;变的是工程界对它们的判断。截至 2026,有三件事必须知道。
① 过度设计:模式用在不存在的变化上
场景 6 的「单实现策略」是一类。同类的还有:为每个普通对象都套一个工厂、为只有一个家族的产品预先搭抽象工厂、为不会再加步骤的对象引入建造者。它们的共同失败模式是:为了「显得专业」而引入间接,但被隔离的那处变化根本不存在。
模式是对已出现或高度可预期的变化的回应,不是预先铺设的脚手架。看到一个接口只有一个实现、且没有第二个的迹象,默认它是多余的间接,而不是「好设计」。先内联,等变化真的来了再重构。
② 语言特性吸收了一批模式
GoF 成书时,主流语言(C++、早期 Java)缺少一些今天习以为常的特性。当年要靠一个模式(一堆类)才能表达的东西,现在一行语言特性就够了。Peter Norvig 1996 年就指出:23 个模式里有 16 个在动态语言里「隐形或更简单」。这不是新观点,而是已成立近三十年的判断。
| 语言特性 | 吸收了哪个模式 | 今天的写法 |
|---|---|---|
| 一等函数 / lambda | 策略、命令、模板方法的钩子 | 传一个函数,而不是定义一个接口 + 一族类 |
语言内置迭代器(for-of / 生成器) | 迭代器 | 直接用语言的迭代协议,不手写迭代器类 |
| 依赖注入(DI)容器 | 工厂家族、单例 | 容器管理对象创建与生命周期,不手写工厂 / 单例 |
| sealed 类 / enum + 模式匹配 | 状态(简单状态机) | 用代数数据类型 + match 表达状态,无需一族状态类 |
「被吸收」不等于「这个模式错了」。模式背后的意图(可互换的算法、统一的遍历、受控的创建)依然成立,只是落地的代码形态从「一堆类」变成了「一个语言特性」。读意图的能力在这里照样管用——lambda 之所以能替代策略,正因为它实现了策略的意图。
③ 一个模式已被取代:单例
单例是整个目录里判断翻转得最彻底的一个。它一度是「最好教的模式」(02 章有完整剖析)。截至 2026,它被广泛当作反模式:私有构造 + 静态访问这套机制本身,就是隐藏的全局状态,破坏单元测试隔离、把耦合藏进了看不见的地方。现代做法是把「全局唯一」交给依赖注入容器去管,而不是写一个 Singleton 类。
但 GoF 没有过时。
没有任何一个可信的声音说「设计模式已经没用了」。即使是最激烈的批评者,也保留模式作为团队的共享词汇:说一句「这里加个装饰器」,胜过三段话描述。而模式背后的原则层——松耦合、面向接口、组合优于继承——是语言无关、时间无关的。
2026 的真实共识是一个「既…又…」:原则永恒,约 16/23 的实现被语言特性吸收。它既不是「GoF 是圣经」,也不是「GoF 已死」。
§自测题库:三层梯度
上面六个场景已经是「应用判别层」。下面补概念层与原理层,凑齐三层梯度。先合上教程,把答案写下来,再展开对照。
概念层(对应 01–04 章)
- 用一句话说出「封装变化」是什么,以及它的两种落地机制。
- 装饰器和代理都「实现与被包对象相同的接口」。把它们分开的两个信号是什么?
- 抽象工厂和工厂方法,一个用组合、一个用继承——分别是哪个用哪个?
原理层(对应各章「底层机制」)
- 为什么说单例的「测试不友好」是它机制的内禀属性,而不是用错了它?
- 组合(Composite)被说成「明知故犯地违反接口隔离原则」,具体违反在哪里?这个权衡换来了什么?
- 模板方法和策略都能「替换算法」。为什么说一个在编译期固定、一个在运行期可换?
应用判别层 · 加试
- 一个日志库:用户可以决定日志「先脱敏、再压缩、再加时间戳」,顺序和组合任意。这是装饰器还是责任链?给出判断信号。
- 你看到一段代码:
PaymentFactory里一个switch(type)返回不同支付对象,支付类型还在不断加。这是「简单工厂」。要不要升级成工厂方法或抽象工厂?依据是什么?
合上整份教程,凭记忆在纸上重画起点的概念地图:顶部的引擎是什么、中间那条因果链怎么推到「读意图」、底下分哪三组。画完翻回 0.4 对照——你画的图里,有没有那条横跨三组的「双胞胎」带?如果漏了它,说明主线还没真正长进你的记忆里,回 01 章再走一遍 §1.4。
答案(先把三层都做完再展开)
概念层
- 把一处将来会改的代码隔离到稳定接口后,使其能独立替换。两种机制:面向接口编程、组合优于继承。
- ① 接口是否改变——都不变,但代理意图是控制访问、装饰器意图是加职责;② 谁控制组合与生命周期——装饰器由客户端叠加,代理自己管理被代理对象的生命周期。
- 抽象工厂用组合(一个工厂对象挂多个 create 方法),工厂方法用继承(子类重写 create)。抽象工厂内部常常正是用工厂方法实现的。
原理层
- 因为「私有构造 + 静态全局访问」这套机制本身就引入了全局状态:测试时无法替换 / mock 它,且实例状态跨测试用例残留。问题出在机制,不在用法,所以换个用法救不了。
- 违反点:
add()/remove()这种「管理子节点」的方法被放进了叶子和容器共享的接口,而叶子根本没有子节点。换来的是:客户端能统一对待叶子和容器,递归遍历整棵树时无需区分类型。 - 模板方法用继承:步骤由子类在编译期重写,顺序被父类锁死,运行时换不了。策略用组合:算法对象在运行时被持有,可随时替换。
应用判别层 · 加试
- 装饰器。信号:每一层都「实现相同接口 + 持有上一层」,可任意叠加顺序,且输出仍是同一个日志接口。责任链的意图是「让一个请求沿链传递,直到某个处理者拦下它并终止」——日志这里没有「拦下并终止」,每一层都要执行,所以是装饰器不是责任链。
- 看「变化是哪一类」:支付类型在单维度持续增加,且每种类型是单个产品 → 工厂方法即可(每种支付一个创建者子类,新增类型不改旧代码)。只有当出现「整族相关产品要保持一致」(如同时换一整套支付+对账+发票实现)时,才升级到抽象工厂。别为了升级而升级——若
switch改动频率低、类型增长平缓,简单工厂留着也没错。
给一段真实代码做「模式体检」
找一个你正在维护的真实项目,挑一个超过 200 行、改动频繁的类或模块。用本教程的眼光给它做一次体检,回答三个问题:(1) 这里真正会变的是哪一处?(2) 它现在有没有被隔离到一个接口后面,还是和不变的部分焊在一起?(3) 如果要重构,该上哪个模式——还是说,它其实不需要模式,只是需要把一个函数拆开?
提示(卡住再展开)
最难、也最值钱的是第 (3) 问。新学完模式的人倾向于到处看见模式;但很多「坏代码」的正解是「拆个函数、起个好名字」,而不是「上一个模式」。判断分界:只有当那处变化会以多种形态反复出现(多种算法、多种产品、多个状态)时,才值得用模式把它隔离;一次性的复杂逻辑,拆函数就够了。能分清这两者,你就越过了「为模式而模式」这道坎。