设计模式 39:从变化需求保留必要模式
一条订单用例原来处理原价和会员九折,审核挡下被封禁订单,然后只生成邮件收据。现在要允许清仓八折,收据可选择短信,还要保持“拒绝的订单不生成收据”。要为每个变化都套 Strategy、Bridge、责任链、Facade 吗?先把新旧行为跑出来,再数清新增的边界由谁维护。
从一个可检查的旧入口出发
labs/39 的 Before.checkout 在 1000 分上分别得到 email|receipt/1000 和 email|receipt/900,被拒绝返回 DENIED;它对清仓规则与短信渠道都抛异常。两条新需求是独立的:新增清仓时不能改变会员九折的金额,新增短信时不能改变邮件收据文本。负金额由新入口拒绝、未知渠道也显式失败;拒绝时不应调用报价策略,测试用会抛 AssertionError 的策略固定了这个顺序。
最小修复不等于保留所有模式
After 复用本系列已有的 labs/28 价格规则接口和报价入口,不再新写一个互不关联的策略框架;用构造参数注入原价、会员或清仓规则。仅有两种固定收据渠道且没有渠道自带生命周期时,switch (channel) 就足够,不额外引入 Bridge 的通道层次;只有封禁和固定限额判断时,直接 if (blocked || base > maximumBase) return "DENIED",不为它建立责任链或 Facade。结果约束检查负报价输出,阻止坏策略将负金额写进收据。Alternative 全放在一个函数的 switch 中,对只有一个入口的项目更短;若价格规则将独立扩展,After 的接口降低对用例入口的改动。
1 | |
flowchart LR
Caller[用例调用者] --> Order[39 订单入口]
Order --> Rule[28 报价策略合同]
Order --> Local[本地封禁与限额判断与邮件/短信分支]
Rule --> Amount[金额值]
这是本仓库编译后的依赖方向,不是图画出来就成立:lab-39 的 POM 显式依赖 lab-28,本地 jdeps -verbose:package -cp labs/28/target/classes labs/39/target/classes 记录从 lab39 包指向 lab28 包;并没有依赖 lab22 的 Bridge、lab32 的责任链或 lab25 的 Facade。没有源码 SHA 可被冒充为外部库实测,本例所有修改都在本仓库当前代码里。
| 变化与合同 | Before |
After 的实际改动边界 |
Alternative 的改动边界 |
|---|---|---|---|
| 原价和会员邮件 | 已有、共同回归仍通过 | 组装不同 Rule;订单入口不改价格计算 |
两个价格分支 |
| 清仓八折 | 旧入口拒绝 | 新规则交给报价合同,不碰渠道选择 | 新增价格分支 |
| 短信收据 | 旧入口拒绝 | 在固定的两渠道分支增一种 | 增加渠道分支 |
| 封禁订单 | 直接返回 DENIED |
校验输入后先拒绝,不调报价 | 同样直接拒绝 |
保留 28 的报价规则,是因为已经有三种算法可在同一合同下选择;移除本场景未必要的通道层次、审核链、工厂与代理,是因为需求里没有第三方协议、动态审核配置或复杂交互。这里的“改动位置”只指这些合成教学需求在仓库内的代码/测试,不是测过的真实团队变更成本。若下一次真的引入第三方发送协议或多位审核者,再从失败测试出发恢复相应边界;不能因为之前章介绍过某模式就把它常驻在最后设计里。
第三次变化:审批增加原价限额
新增规则和短信渠道通过旧合同回归后,审核又增加“原价大于 1500 分时拒绝”的需求。After(pricing, maximumBase) 保存明确的上限,原先的单参数构造器把上限设为 Integer.MAX_VALUE,保持旧调用方式。输入验证后先判断封禁与限额,再报价和生成收据;负上限在组装时拒绝。
1 | |
1500 分恰好通过,清仓短信为 sms|receipt/1200;1501 分返回 DENIED。换为邮件仍为 email|receipt/1200。另一个会抛 AssertionError 的报价函数验证超限时根本不会执行价格计算;简单分支替代方案对同一限额得到相同结果。普通会员订单的 101 分邮件回归得到 90 分,防止整数舍入变化被整千金额掩盖。
审批变化只增加一个构造参数和拒绝条件,不修改 lab28 的价格规则,也不修改渠道分支。因此没有为了增加第二个固定条件引入可配置责任链。若审核者需要独立部署、动态排序或不同失败恢复,再重新衡量审核端口与链的成本。这一取舍由当前需求和测试边界支持,不代表所有审批业务都应写在用例函数内。
| 增量 | 保护的旧合同 | 实际新增边界 |
|---|---|---|
| 清仓价格规则 | 原价、会员九折向下截断 | lab28 清仓算法及组装选择 |
| 短信渠道 | 邮件收据文本与拒绝短路 | lab39 渠道分支 |
| 原价审批限额 | 旧单参数组装和封禁拒绝 | lab39 上限参数、阈值条件及新断言 |
实验、边界与练习
在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/39 -am test、./mvnw -B -ntp verify;输入、环境、退出码、原始输出及包依赖实测在 examples/design-patterns/evidence/39/RUN.md。本章是字符串收据与纯函数报价,不发送通知、不保存订单,也不因 00–39 累计回归通过就推断事务、支付或并发安全。全系列旧章节合同必须在最终回归中保持,而整站 Markdown/Hexo 能否无错误构建要单独验证,不能当成 Java 运行时通过。
- 新增“第三个渠道接入不兼容的外部签名协议”,先对旧邮件/短信金额和封禁短路加回归,再写协议错误转换断言,判断需要 21 的 Adapter 还是 22 的 Bridge;记录真正要改的文件和运行输出。
- 删除清仓需求、只保留原价与会员规则。比较仍注入
Rule与把两条固定计算直接写进订单函数,运行旧合同后给出保留或移除抽象的可证伪理由。
本次补全增加的实验在 examples/design-patterns/evidence/local-completion-20261003/RUN.md 留存本地命令与原始输出;各章原云端日志保留其历史版本边界。
参考资料
- Strategy 模式出版社二手摘录,Pearson/InformIT:https://www.informit.com/articles/article.aspx?p=1398602 。
- 综合来源边界:
writing-plans/design-patterns/SOURCES.md。 - 实验代码:
examples/design-patterns/labs/39/。
上一节:38 折扣表达式需要语法树吗;下一节:E01 高阶函数替代了什么。






