适用读者:背得出 23 个模式名字、但被问「你项目里为什么用这个」就卡住的人;以及正在给团队定「什么时候该抽接口」的人。
前置:S04(Java 语言与核心 API:接口、泛型、反射、动态代理)。
覆盖旧文:A-2379(前言/总结部分)、A-2392(分类图解部分)、A-2609(概念与分类)、A-2326("了解哪些设计模式")、A-950(71 点)、A-550(58 点)、A-551(235 点)——合计 581 点。
扩展点:E-S18-001(选型判据)、E-S18-027(七段流程骨架)。
下一篇:S18-J02 · 创建型模式:对象怎么来。
1. 一句话结论
设计模式不是一份要背完的清单,而是一套"把变化点关进一个小盒子"的模板;选型的唯一硬判据是:变化确实会发生、且发生的频率值得你多写一层间接。
旧文 A-950 给的五个好处(复用、可维护、扩展、沟通、少踩坑)都对,但它们全是结果,不是理由。理由只有一个:你已经能指名道姓地说出"这里会变"。
三条前提,缺一条就别上模式:
| 前提 | 检验方式 | 不满足的后果 |
|---|---|---|
| 变化真的会发生 | 过去 6 个月这个分支改过几次?只改过 1 次 → 不上 | 为一个假想需求付永久维护成本 |
| 团队读得懂 | 新人能否在 10 分钟内说出这层间接的作用 | 抽象变成"只有作者懂的迷宫" |
| 有测试兜底 | 换成策略/工厂后,能否一条命令跑绿 | 重构后无人敢改,抽象反而冻结了代码 |
判据(一句话版):"如果我明天把这段 if-else 换成注册表,谁会因此少改代码?" 答不出具体的人或具体的下一次需求,就先别换。
2. 三类模式的划分逻辑
GoF 在 1994 年的《设计模式:可复用面向对象软件的基础》里给了 23 种,按意图分三类。分类的价值不在记忆,而在缩小搜索空间:你先判断"我要解决的是对象的创建、组合,还是通信",再看那一类里的 5–11 个候选。
| 大类 | 关注点 | 一句话 | 成员(23 种) |
|---|---|---|---|
| 创建型 5 | 对象怎么来 | 把"new 哪个类"从调用方搬走 | 单例、工厂方法、抽象工厂、建造者、原型 |
| 结构型 7 | 对象怎么拼 | 用组合替代继承,拼出更大的结构 | 适配器、桥接、组合、装饰、门面、享元、代理 |
| 行为型 11 | 对象怎么通信 | 把"谁做、按什么顺序做"变成可替换的部件 | 责任链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者 |
记忆口诀"建、拼、动"(创建 / 结构 / 行为)出自 A-950,够用。但请记住一句更重要的:分类是 GoF 的整理方式,不是你的设计依据。真正的依据是下一节的变化点。
A-2392 用"一句话题眼"把 23 种压成一行一个(抽象工厂=多套方案、建造者=善于分解、原型=传承知识、单例=专注……),这种压缩适合回忆,不适合决策——因为题眼描述的是"这个模式像什么",不是"你什么时候该用它"。决策请用 §3 的表。
3. 选型判据:变化点定位法
把"我想用个模式"换成"我的变化点是什么",五问过一遍:
- 变的是什么? 是"选哪个实现"(→ 策略/工厂)、"加不加行为"(→ 装饰)、"流程哪一步不同"(→ 模板方法)、"接口对不上"(→ 适配器)、"要不要经过我"(→ 代理)、"先做哪一步"(→ 责任链)。
- 变化频率? 一季度一次以上值得封装;一年一次不值得。
- 谁受影响? 改动会波及几个调用方?只有一个 → 别动结构。
- 能不能用更便宜的手段解决? 配置表、枚举 + switch、一个 Map 注册表,往往比一整套接口 + 五个实现类便宜。
- 引入后测试怎么写? 写不出测试,说明边界没划清。
| 你观察到的变化点 | 首选候选 | 次选 / 更便宜的替代 | 典型反例(不该用) |
|---|---|---|---|
| 按类型 new 不同实现,分支还会增加 | 工厂方法 + 注册表 | 简单工厂(一处 switch) | 只有两个分支且一年没变 |
| 同一动作有多种算法,运行期切换 | 策略 | 枚举 + switch(分支 ≤3) | 算法之间要共享大量状态 |
| 流程固定、个别步骤不同 | 模板方法 | 传函数(Java 8+ 的 Function) |
步骤顺序本身会变 |
| 要给对象层层加能力,且可任意组合 | 装饰 | 一个配置对象 + 布尔开关 | 只加一种能力 |
| 访问前要鉴权/计数/懒加载 | 代理 | 框架注解(Spring AOP) | 只是想做日志,用切面更省 |
| 两个接口语义相同、签名不同 | 适配器 | 一层静态转换方法 | 两边都能改(那就统一接口) |
| 一次事件要通知多方 | 观察者 | Spring ApplicationEvent |
通知顺序需强保证(用消息队列) |
| 请求要按顺序过一串处理者 | 责任链 | 一个 List + for 循环 + 早退 | 处理者集合固定且只有 2 个 |
| 全局只需一个实例(连接池、配置) | 单例 | 交给容器(Spring 默认单例 Bean) | 用单例藏全局可变状态 |
"更便宜的替代"这一列是本篇的重点:S18 的立场是——能用语言特性(函数、枚举、记录类)解决的,不要动结构。Java 17 的 record、sealed interface、switch 模式匹配,已经把很多 2005 年必须靠模式解决的问题降维了。
4. 模式不是教条:过度设计的四种表现
A-950 的"常见误区"讲了三条(过度设计、生搬硬套、为面试背模式),这里把它变成可观测的信号:
| 表现 | 现场信号 | 处置 |
|---|---|---|
| 接口只有一个实现,且没有第二个的排期 | grep "implements XxxService" 只有 1 行 |
先写具体类,等第二个实现出现再抽 |
| 抽象层的命名比业务还难懂 | AbstractStrategyFactoryBean |
改名或删除;命名难度=抽象错位的信号 |
| 为了"符合开闭"把 switch 拆成 5 个类,结果新增分支仍要改注册表 | 每次加分支仍改 2 个文件 | 收益是"改 1 处"才值得拆;改 2 处不如不拆 |
| 调试栈深了 6 层才到业务代码 | 断点要打 5 次才进到 doPay() |
加一层代理/装饰要有明确的横切理由 |
A-2326 里那道经典面试题——"导出功能要支持 Excel、JSON、XML,你怎么设计"——标准答案常是"工厂 + 策略"。但真实工程里还有第三种答案:一个 Exporter 接口 + 一个 Map<String, Exporter> 注册表,新增格式=新增一个类 + 一行注册,连工厂类都不用写。这就是"工厂模式"在 Java 17 时代的最小形态。
下面这段是实测(Java 17.0.20,2026-11-06,单文件 java Patterns.java 直接运行):
// 注册表式工厂:对扩展开放的最小形态(无需工厂类层级)
static final Map<String, Supplier<PayChannel>> REG = Map.of(
"alipay", Alipay::new, "wechat", Wechat::new, "card", Card::new);
static PayChannel channel(String code) {
var s = REG.get(code);
if (s == null) throw new IllegalArgumentException("unknown channel: " + code);
return s.get();
}
实测输出:
[factory] card fee=150
[factory] reject unknown channel: unionpay
注意第二行:未知渠道明确报错,而不是静默返回 null。这是判据的一部分——抽象层必须对"没覆盖的情况"有明确行为,否则你只是把 if-else 换了个地方藏。
5. 从模式到骨架:两个可复用的骨架
模式解决"一个变化点",骨架解决"一整条流程怎么摆"。旧文里有两份可直接搬用的骨架,都属 L2/C09(原专题补充):
(1) 一次性脚本 / CLI 的七段骨架(A-551,E-S18-027)
| 段 | 职责 | 为什么单独成段 |
|---|---|---|
| ① 入口 | 极薄:接参数、派活、把结果换算成退出码 | 让"干活"部分可以不经命令行被测试 |
| ② 参数解析 | 先确认"试跑还是真跑、用哪份配置" | dry-run 是最便宜的安全阀 |
| ③ 配置加载 + 校验 | fail-fast,不带病上路 | 把失败挪到最便宜的时刻 |
| ④ 进程互斥 | 单实例锁 | 防重复执行(幂等的外部保障) |
| ⑤ 初始化资源 | 建目录、开日志、连外部 | 统一"用完必还"的边界 |
| ⑥ 核心循环 | 逐条处理 + 去重 + 成败分级 | 唯一有业务逻辑的地方 |
| ⑦ 收尾与退出码 | 无论成败都落盘 + 通知 + 给结论 | 让调度器/CI 能判断结果 |
这张表的价值在于:它把"可靠性"拆成了七个可检查的位置。你写任何批处理(导数据、发通知、跑对账)都可以照着过一遍。
(2) 宿主 / 插件双线程骨架(A-550)
- 两个边界:宿主(稳定、持有资源)与插件(易变、装载业务);
- 一条契约:两边只通过消息对象通信,不共享可变状态;
- 一个反模式:插件直接调宿主的内部服务——这等于把"易变"注入"稳定",隔离白做。
这份骨架和"适配器 + 门面"是同一个道理的不同尺度:适配器隔离接口差异,双线程骨架隔离生命周期差异。
6. 常见误区
| 误区 | 真相 | 依据 |
|---|---|---|
| "23 种都要会写" | 常用 8–10 种;剩下的是"读得懂别人代码"的要求,不是"必须自己写" | A-950;本篇 §3 |
| "用了模式就是好代码" | 模式的成本是间接层;没有变化点就是纯负债 | A-950 常见误区 |
| "单例能保证唯一" | 只在单 ClassLoader + 单 JVM 内保证;分布式下要外部协调 | 见 J02 §2 |
| "设计模式 = 架构" | 模式是"工程实现的血肉",分层/边界才是骨架 | A-1538 原文措辞 |
| "面试要报模式名" | 面试官要听"场景→问题→方案→效果"四段 | A-950 高频追问 |
7. 自测
- 你项目里最近一次"加一个分支"的改动,改了哪几个文件?如果 >2 个,抽象的位置对吗?
- "只有两个实现"的接口,你会现在抽还是等第三个?给出你的判据。
- 装饰器和代理都实现同一接口,区别落在"意图"的哪一面?
- 七段骨架里,为什么"试运行"要放在第②段而不是第⑥段?
- 宿主/插件骨架中,插件直接调用宿主内部服务会导致什么?
- 用一句话说明:什么样的情况下你不会引入任何设计模式?
下一篇
S18-J02 · 创建型模式:对象怎么来
总论给了判据,下一篇把它落到第一类:单例的四种写法与容器时代的位置、工厂三兄弟的边界、建造者与不可变对象、原型的深浅拷贝代价。所有示例都在 Java 17 上跑过,输出贴在正文里。
本篇属于系列《S18 · 架构方法与领域建模》。旧文来源:A-2379/A-2392/A-2609/A-2326/A-950/A-550/A-551;扩展点 E-S18-001、E-S18-027。示例为 Java 17.0.20 实测(临时目录单文件运行),非 Spring 环境。