配套代码: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种模式更有学习价值。需要回看某种职责划分时,从系列导航进入相应源码与练习。