26|设计模式选型与综合复盘:把变化、契约和测试连起来
配套代码:GitHub 仓库与运行说明 · 24种模式源码目录。使用 JDK 25、Gradle Wrapper 9.6.1,包名前缀为
com.hanserwei.patterns。
系列导航:Java 25 设计模式学习指南
上一篇:访问者 · 下一篇:系列终点,返回导航选择复习专题
学完24种模式之后,最容易出现的新问题是“任何需求都想套模式”。一个小服务被拆成很多接口,每个实现都只有一处使用,修改时却仍然到处联动。模式没有消除复杂性,只改变了复杂性所在的位置;是否值得引入,要看这份改变是否服务于真实需求。
本篇以文章发布与编辑为线索进行架构推演,不新增一个必须运行的综合应用。前24篇的包彼此独立,同名类只代表各章局部模型;不要直接拼接它们,然后把教学对象当成已经具备事务、持久化和并发保证的完整内容平台。
先描述变化,再选择模式
拿到“增加一种导出格式”时,先问变化在哪里:只是格式化算法不同,还是创建产品的方式不同?需要一次切换一组匹配的产品,还是固定流程中的某个步骤?这些答案分别可能指向策略、工厂、抽象工厂和模板方法。
把需求写成一句可验证的话,例如“新增按字数计费,不修改报价服务”“给已有文本增加去空白能力,保持调用接口”“草稿无法归档,失败后状态不变”。句子里应该包含变化和保持稳定的部分,这样才有办法评价模式是否成功。
| 变化或约束 | 优先观察的模式 | 需要付出的代价 |
|---|---|---|
| 少量产品通过配置名称集中选择 | 简单工厂 | 新类型通常修改工厂分支 |
| 固定创建者流程由子类选择产品 | 工厂方法 | 创建者与产品两组类型增加 |
| 一次切换多个兼容组件 | 抽象工厂 | 增加产品种类影响全部工厂 |
| 分步收集参数并创建有效对象 | 建造者 | 多出构建阶段及校验时机 |
| 基于已有对象继续配置 | 原型 | 必须维护复制深度与身份规则 |
| 一个类型确实需要共享唯一实例 | 单例 | 全局依赖、测试替换与作用域限制 |
| 已有接口与业务契约不一致 | 适配器 | 参数、单位、异常等映射需要维护 |
| 两条类型维度独立组合 | 桥接 | 增加实现接口及装配关系 |
| 单个元素与树形整体统一操作 | 组合 | 环、共享节点和递归成本要明确 |
| 保持接口并叠加行为 | 装饰器 | 顺序与资源所有权更复杂 |
| 调用方需要统一业务入口 | 外观 | 不能自动获得事务和幂等性 |
| 大量重复的稳定细粒度状态 | 享元 | 完整缓存键与生命周期管理 |
| 控制真实对象访问 | 代理 | 缓存一致性、权限或远程失败语义 |
| 请求依次经过可组合处理者 | 责任链 | 顺序、短路和无人处理语义 |
| 请求需要延迟执行或管理撤销 | 命令 | 执行历史、失败恢复与幂等性 |
| 小语言需要对象化表示与求值 | 解释器 | 语法树规模与解析器实现成本 |
| 遍历过程应独立于集合表示 | 迭代器 | 快照、实时视图或 fail-fast 的选择 |
| 多对象交互规则形成网状依赖 | 中介者 | 协调者容易膨胀 |
| 外部保管对象历史状态 | 备忘录 | 快照复制和历史空间成本 |
| 一个事实需要通知多个兴趣方 | 观察者 | 生命周期、顺序与异常传播 |
| 行为随合法生命周期变化 | 状态 | 状态类数量及并发迁移一致性 |
| 同一任务有多套可替换算法 | 策略 | 算法契约与选择逻辑仍需维护 |
| 固定流程只有有限步骤变化 | 模板方法 | 继承耦合与扩展点约束 |
| 元素稳定、操作经常增加 | 访问者 | 新元素要求修改访问者接口及实现 |
这张表是定位问题的入口,不是看到关键词就自动选择模式的决策器。比如“缓存”既可能是代理的访问策略,也可能是享元工厂的实现细节;判断时仍要看你想保护的契约和变化方向。
最容易混淆的模式,换成具体问题比较
| 模式对照 | 关键提问 | 示例中的证据 |
|---|---|---|
| 简单工厂 / 工厂方法 | 谁选择产品,参数分支还是创建者子类? | create(type) 与 createFormatter() 的职责不同 |
| 工厂方法 / 抽象工厂 | 是一个创建步骤,还是匹配的产品族? | Formatter 与 Button + Banner 的变化轴不同 |
| 适配器 / 桥接 | 在翻译已有接口,还是分离两个设计维度? | send → deliver 的翻译,与 Notice × Renderer 的组合 |
| 装饰器 / 代理 | 调用方在组合增强,还是把目标访问交给代表? | 文本顺序转换,与缓存命中跳过真实读取 |
| 外观 / 中介者 | 为外部提供入口,还是协调同事交互? | 子系统不知道 PublishingFacade,Reviewer 知道 ReviewRoom |
| 单例 / 享元 | 唯一身份,还是共享大量对象的重复状态? | 一个 SitePolicy,多种按键共享的 TextStyle |
| 策略 / 状态 | 算法被选择,还是生命周期在迁移? | FeePolicy 由构造器选择,PublicationState 由动作推进 |
| 策略 / 模板方法 | 替换算法,还是覆写固定流程步骤? | QuoteService 的组合,与 ArticleExporter 的继承 |
| 命令 / 备忘录 | 保存请求意图,还是保存对象状态? | AppendCommand 保存 suffix,Snapshot 保存恢复数据 |
| 迭代器 / 访问者 | 如何遍历,还是访问元素后做什么? | iterator 管游标,accept 管双分派 |
同一个对象结构可能同时服务于多种模式。抽象工厂内部可以调用工厂方法,模板方法的某个步骤可以委托策略,命令可以保存备忘录,组合树可以被迭代器遍历再交给访问者处理。组合模式应当围绕明确职责发生,而不是为了让架构图出现更多名称。
推演一个文章发布用例
假设目标需求是:编辑草稿,可以撤销修改;发布前校验标题和权限;只有草稿允许发布;发布成功后通知关注者。先不用全部24种模式,只挑能够解释这几个变化点的职责。
flowchart TD
User[编辑者] --> History[命令历史]
History --> Draft[草稿内容]
User --> Publish[发布用例入口]
Publish --> Checks[标题和权限校验]
Checks --> Lifecycle[检查当前生命周期]
Lifecycle --> Store[保存发布结果]
Store --> Events[发布成功事件]
Events --> Inbox[订阅者收件箱]
Events --> Analytics[统计订阅者]
命令历史负责用户操作顺序,可以借助草稿自己的备忘录恢复状态。发布入口适合用外观式应用服务组织步骤;规则有多种组合需求时引入责任链;生命周期行为逐渐复杂时再引入状态对象;通知的兴趣方经常增加时使用观察者。
首先要固定失败顺序:标题或权限失败时,不修改草稿状态,也不产生发布事件。如果先设置 published 再校验,失败时就必须额外恢复;这是流程设计问题,不能靠给类加上 State 后缀自动解决。
其次要固定成功含义:保存完成与通知完成是否必须同时成功?如果监听器失败,文章应该仍然保持已发布,还是整个发布返回失败并回滚?前文观察者选择同步、失败即中断;它不能直接提供数据库事务回滚。真正持久化时,可以把文章更新和待投递事件放进同一事务,再由独立流程投递,但这是额外的可靠性设计。
最后要固定重复请求:用户双击发布按钮时,是第二次拒绝、返回第一次结果,还是产生重复事件?状态检查能表达“当前不允许重复发布”,却不能单独处理两个并发请求都读到草稿的情况;需要存储层条件更新、版本号或其他一致性机制。
用失败矩阵检查方案
| 场景 | 希望保持的不变量 | 最小验证方式 |
|---|---|---|
| 标题为空且无权限 | 不保存、不迁移、不通知;错误优先级确定 | 调换规则顺序,断言首个错误及副作用计数 |
| 保存操作失败 | 不发布“成功”事件 | 用失败存储替身,断言监听器未调用 |
| 监听器失败 | 按事先定义的策略保留发布结果或报告失败 | 成功、失败、成功三个监听器组合 |
| 两次编辑后撤销一次 | 回到第一次编辑后的状态 | 断言中间值,而不只断言最终为空 |
| 创建快照后继续编辑 | 旧快照内容不变 | 修改可变集合元素,检查隔离深度 |
| 同一请求重复提交 | 不出现意外重复业务结果 | 重放请求并核对结果与事件数量 |
并发和持久化测试需要与真实存储语义配套;仅靠内存替身不能证明数据库的隔离性或消息交付可靠性。本系列的49项测试证明的是各独立示例承诺的行为,上表是你扩展综合系统时应继续补上的验收方向。
何时应该删掉一个模式
如果接口只有一个实现,且没有替换、边界隔离或测试的需要,可以考虑去掉它。如果一个工厂只把参数原样交给构造器,没有选择、校验或生命周期职责,直接调用构造器可能更清楚。如果一个状态类只是返回字符串,而所有判断还在上下文里,应该先把职责理顺。
也不要因为暂时只有一个实现就一律删接口。适配外部 SDK、跨模块提供稳定契约,或隔离真实副作用时,一个实现仍可能有明确的边界价值。判断依据是它减少了哪种依赖,而不是机械地数实现类数量。
每次重构记录三件事:需求改动前后需要改几个位置,调用者是否更容易理解和测试,新增抽象是否引入了难以管理的生命周期。用这些事实替代“更优雅”“更符合设计模式”这样的笼统评价。
最后的实践任务
选择你自己的一个小功能,例如周刊草稿编辑或多格式导出,先用最直接的 OOP 实现完成行为测试。接着增加一个真实变化:另一种导出格式、多个独立通知订阅者,或一条新的生命周期规则。
只有当改动开始穿透多个不相关位置时,再引入相应模式。提交前运行 ./gradlew check,保留类、字段、方法及测试注释,并在一段说明里回答:变化点是什么,哪些代码从此保持稳定,代价是什么,还有哪些并发或持久化边界尚未实现。
能用这段说明说服未来维护代码的人,比在一个项目里集齐24种模式更有学习价值。需要回看某种职责划分时,从系列导航进入相应源码与练习。