设计模式 15:阅读模式先看变化
小额收据原先只显示在屏幕或邮件里,现在审计系统需要 audit/200 的格式。旧方法用渠道名分支,对 audit 抛错。把分支拆成三个类就能叫“策略模式”吗?仅看类图答不了这个问题:先问是谁选择格式、什么行为保持不变、下一次变化要修改哪里。
先补缺失的行为
Before.receipt(200, "audit") 的断言是抛 IllegalArgumentException,刻意保留旧功能缺口。最小修复可以在原 switch 中加一支:Alternative 这样做,还加上负金额拒绝,并保留未知渠道报错。如果渠道是封闭的小集合,需求只来自一个调用位置,这就是合理的实现;没有理由为展示 GoF 模式专门建类层次。
若渠道格式来自调用者的配置,After 接收一个 IntFunction<String>:
1 | |
协作方向是调用者选择格式函数 → 收据入口检查负金额 → 格式函数生成文本。变化点从 switch 移到组装位置;选择逻辑没有消失,只是换了归属。这里的可插拔函数具有与 Strategy 相似的变化边界,但不等于凡是传 lambda 都实现了 GoF 模式。阅读一个模式应找出意图、稳定流程、参与者的调用关系和变化可能扩散的路径;下一节将用“创建对象由谁决定”的具体问题检验 Factory Method,而不是只比较方法名字。
| 方案 | 新增 audit 时 |
失败行为 |
|---|---|---|
Before |
旧渠道分支拒绝新增渠道 | 旧异常由反例断言记录 |
After |
提供新格式函数;调用者负责选择 | 入口拒绝负数;函数自己的异常不被吞掉 |
Alternative |
添加 audit 分支 |
拒绝负数与未知渠道 |
这三个方案只对200 分的收据文本共享旧合同,不声称 Before 对负数也做了校验。若格式还需连接数据库或外部队列,应重新定义失败和重试契约,不能拿这个纯函数例子当生产消息投递保证。
flowchart LR
Caller[调用方] --> Branch[Before 本地分支]
Caller --> Context[After]
Context --> Fn[IntFunction formatter]
Caller --> Fixed[Alternative 固定输出]
用行为而不是名称验收
在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/15 -am test 和累计 ./mvnw -B -ntp verify,实际退出码、环境、原始输出见 examples/design-patterns/evidence/15/RUN.md。断言覆盖屏幕和邮件旧文本、新渠道、负金额和未知渠道;既未测 IO 失败也未测渠道运行时加载。
练习:
- 新增
sms/200文本及“负金额不能发出”的需求,先让三个方案分别跑旧渠道合同与新增断言,再记录修改入口、调用者和格式函数的位置。 - 将三个格式合并到一个
switch,删掉函数注入,并用测试证明行为仍相同;列出何种外部配置需求出现时再恢复可注入边界。
参考资料
- GoF 作者回顾:
A Look Back: Why We Wrote Design Patterns,https://www.informit.com/articles/article.aspx?p=1327762 。 - Gamma/Helm/Johnson 访谈:
Design Principles from Design Patterns,https://www.informit.com/articles/article.aspx?p=1404056 。 - 实验代码:
examples/design-patterns/labs/15/。
上一节:14 包依赖为什么成环;下一节:16 创建决策何时交给子类。






