报价原来只有原价与会员九折,现在新增清仓八折。只把三段算式拆成三个类,不检查它们是否都遵守同一输入约束,调用方仍无法放心替换算法。更重要的是谁选择“会员还是清仓”:策略对象并不会自己知道当前订单属于哪一类。

共用入口、分别验证算法

labs/28 的 Before 在一个 switch 处理原价和会员;未知清仓抛错。After 注入 Rule.apply(cents),入口统一拒绝负价,再交给对应算法。共同合同套件将原价、九折、八折分别作为参数,逐个断言输入 1000 分分别返回 1000/900/800,0 分均返回 0,负数均在入口拒绝。Alternative 保留一个三分支的直接实现,同一组参数化测试核对结果。

1
2
Scenario.After quote = new Scenario.After(Scenario::clearanceDiscount);
int clearance = quote.quote(1_000);

Strategy 的意图是把可替换的算法置于稳定调用合同之下;注入的 lambda 也可承担策略角色,无须为三行纯算式创建三个类。金额合同规定非负整数分折扣后向下截断。九折使用 Math.toIntExact((long) cents * 9 / 10),八折使用相同的先升为 long 再乘除方式。101 分九折为 90 分,1 分九折为 0;cents - cents / 10 则分别得到 91、1,不能拿它替代旧算法。共同测试覆盖这些零头金额,并检查 Integer.MAX_VALUE 九折为 1932735282,避免乘法先在 int 中溢出。零头舍入保持属于兼容要求,修正旧乘法的极大值溢出属于单列的缺陷修复。选择逻辑若仍由调用者每处根据名称 switch,并没有凭空消失;应记录选哪一个的归属,而不是宣称 OCP 已自动满足。

方案 新增清仓规则 0 分、负数
Before 旧分支拒绝清仓 未统一验证负数
After 传一个新算法,不改报价入口 0 可报价、负数拒绝
Alternative 增一条明确分支 同一入口检查

只有一个稳定品种时直接计算更短。算法要在运行时按客户场景切换,或调用方真正需要统一合同,才值得引入策略边界。与 29 的 Template Method 不同,这里用组合替换整个价格规则,不是继承固定的步骤骨架;与 33 的 State 也不同,报价规则不会因对象生命周期自动迁移。

flowchart LR
    Caller[调用方选择规则] --> Context[After.quote 检查输入]
    Context --> Rule[Rule.apply]
    Rule --> Regular[原价]
    Rule --> Loyalty[九折整除]
    Rule --> Clearance[八折整除]

验证与练习

运行 ./mvnw -B -ntp -pl labs/28 -am test 与累计 ./mvnw -B -ntp verify;环境、参数化输入、退出码和原始输出见 examples/design-patterns/evidence/28/RUN.md。舍入仅按本章整数分向下截断合同;没有外部配置、并发选择或真实清仓案例。本地补全的零头金额和极大值回归见 examples/design-patterns/evidence/local-completion-20261003/RUN.md。

  1. 增加“员工八五折”,先加入同一测试源并验证旧三种算法不变;明确订单在哪个入口选择算法,再比较新增分支与新增规则的修改范围。
  2. 把金额模型扩展到 long,先保留当前 1、101、1000 分与 int 最大值合同,再决定乘法溢出时拒绝还是使用大整数,并为两种方案加入相同的边界断言。

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

参考资料

上一节:27 谁能读取延迟加载的库存;下一节:29 哪些报告步骤必须固定。