3129 字
约 10 分钟
11
S18-J01 · 设计模式总论:分类、选型判据与骨架

适用读者:背得出 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. 选型判据:变化点定位法

把"我想用个模式"换成"我的变化点是什么",五问过一遍:

  1. 变的是什么? 是"选哪个实现"(→ 策略/工厂)、"加不加行为"(→ 装饰)、"流程哪一步不同"(→ 模板方法)、"接口对不上"(→ 适配器)、"要不要经过我"(→ 代理)、"先做哪一步"(→ 责任链)。
  2. 变化频率? 一季度一次以上值得封装;一年一次不值得。
  3. 谁受影响? 改动会波及几个调用方?只有一个 → 别动结构。
  4. 能不能用更便宜的手段解决? 配置表、枚举 + switch、一个 Map 注册表,往往比一整套接口 + 五个实现类便宜。
  5. 引入后测试怎么写? 写不出测试,说明边界没划清。
你观察到的变化点 首选候选 次选 / 更便宜的替代 典型反例(不该用)
按类型 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. 自测

  1. 你项目里最近一次"加一个分支"的改动,改了哪几个文件?如果 >2 个,抽象的位置对吗?
  2. "只有两个实现"的接口,你会现在抽还是等第三个?给出你的判据。
  3. 装饰器和代理都实现同一接口,区别落在"意图"的哪一面?
  4. 七段骨架里,为什么"试运行"要放在第②段而不是第⑥段?
  5. 宿主/插件骨架中,插件直接调用宿主内部服务会导致什么?
  6. 用一句话说明:什么样的情况下你不会引入任何设计模式?

下一篇

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 环境。

S18-J01 · 设计模式总论:分类、选型判据与骨架
https://www.clxhxhhr.top/posts/5435/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。