一条订单用例原来处理原价和会员九折,审核挡下被封禁订单,然后只生成邮件收据。现在要允许清仓八折,收据可选择短信,还要保持“拒绝的订单不生成收据”。要为每个变化都套 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
2
Scenario.After order = new Scenario.After(blog.examples.lab28.Scenario::clearanceDiscount);
String receipt = order.checkout(1_000, false, "sms");
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
2
3
var order = new Scenario.After(blog.examples.lab28.Scenario::clearanceDiscount, 1_500);
String accepted = order.checkout(1_500, false, "sms");
String denied = order.checkout(1_501, false, "sms");

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 运行时通过。

  1. 新增“第三个渠道接入不兼容的外部签名协议”,先对旧邮件/短信金额和封禁短路加回归,再写协议错误转换断言,判断需要 21 的 Adapter 还是 22 的 Bridge;记录真正要改的文件和运行输出。
  2. 删除清仓需求、只保留原价与会员规则。比较仍注入 Rule 与把两条固定计算直接写进订单函数,运行旧合同后给出保留或移除抽象的可证伪理由。

本次补全增加的实验在 examples/design-patterns/evidence/local-completion-20261003/RUN.md 留存本地命令与原始输出;各章原云端日志保留其历史版本边界。

参考资料

上一节:38 折扣表达式需要语法树吗;下一节:E01 高阶函数替代了什么。