已有的报价程序对普通订单取原价,对会员打九折。新需求是“大宗订单按八五折”,不是把计算器改造成通用规则引擎。若每加一个折扣都去改同一段条件分支,这段程序就要反复经受回归风险;若需求变成税额与应付额分列,仅在别处加一条折扣策略也解决不了问题。谈“对扩展开放、对修改关闭”之前,要先说清楚保护的是哪种变化。

先给旧流程一个可观察的边界

对合成订单行 paper × 2、单价 10.00 CNY,三种设计的原行为一致:STANDARD 总额 20.00,MEMBER 总额 18.00。Before.quote 只识别会员;调用方提供新增的 BULK 时走默认原价 20.00,测试明确断言这个旧版违约。修补旧版需要改动原有判断。Alternative.quote 则在一个 switch 里补 BULK → 0.85,同样能满足新需求,并不因没有策略接口而被判错。

After.quote 把“某一种折扣对应的倍率”作为 DiscountRule.multiplier(),自己只负责把原价乘倍率并按两位小数舍入:

1
2
3
4
public BigDecimal quote(OrderLine line, DiscountRule rule) {
return line.undiscountedTotal().multiply(rule.multiplier())
.setScale(2, RoundingMode.HALF_UP);
}

新增 BulkRule 返回 0.85,调用方在组装处传入它;原有的 StandardRule、MemberRule 与 After.quote 方法体不需修改。实验用同一调用入口断言新增总额 17.00,会员回归仍为 18.00。这才是本次“关闭修改”的具体范围:倍率实现可以增加,而按倍率计算单行金额的流程保持不动;组装处决定选哪条规则,仍可能需要变更。

flowchart LR
    Caller[调用方选择策略] --> Calculator[After.quote 固定计算流程]
    Calculator --> Rule[DiscountRule.multiplier]
    Rule --> Standard[StandardRule 1.00]
    Rule --> Member[MemberRule 0.90]
    Rule --> Bulk[新增 BulkRule 0.85]

Robert C. Martin 的 The Open Closed Principle讨论通过分离稳定规则与扩展细节减少旧代码修改;Fowler 的 Replace Conditional with Polymorphism是把一类变化分派出去的重构入口。这里不是插件平台:仅有三个教学倍率、调用方显式选择对象,不存在自动发现、热加载或无需组装变更的承诺。若项目只有一种未来折扣且不再增长,单个分支也可能比三个策略类更省维护成本。

另一种变化会打破保护边界

第二张需求卡要求发票分别显示净额、税额、应付额。先前的 DiscountRule 只返回一个倍率,After.quote 只返回一个总额;单靠增加“税率策略”无法凭空让接口返回三项。实验因此在 After 中新增 invoice(line, rule, taxRate),把 quote 结果当净额,单列税额再求和,并引入 Invoice(net, tax, payable)。八五折后的净额 17.00,示例税率 0.10 得税额 1.70、应付 18.70;Alternative 也运行同一新需求断言。这里的税率和舍入都是教学约定,不代表现实税法。

这意味着计算器的源码确实因模型变更而扩展了方法与结果类型;若原接口的含义本来就是“含税总价”,还可能必须修改现有 quote 的语义和所有依赖测试。不能把“新增倍率不改旧流程”偷换成“此后禁止修改任何旧代码”。先识别常见变化方向,再为它留恰好的接缝。

变化压力 Before / Alternative After 的影响 可失败的断言
旧规则:原价/九折 直接分支 固定流程 + 两个倍率类 20.00 / 18.00
新折扣:八五折 原分支须增加判断 新增 BulkRule;After.quote 不变 17.00,会员仍 18.00
新模型:净额、税额、应付额 需要发票计算 新增 After.invoice 与 Invoice,不是只新增倍率 17.00、1.70、18.70

验证与练习

2026-10-02 在 Ubuntu OpenJDK 21.0.12.1、Maven Wrapper 3.9.9 下执行 ./mvnw -B -ntp verify,退出码 0;08 的 7 次测试及 00–07 的 60 次回归无失败。输入、具体类、构建命令和原始输出见 examples/design-patterns/evidence/08/。只验证同步、单行的合成输入;没有处理多券叠加、跨币种、退货或真实税务系统。

  1. 新需求“节日倍率 0.75”:先断言总额 15.00、九折仍 18.00,再分别用 Alternative 新增分支和 After 新增 HolidayRule。列出实际修改的实现文件及组装位置,不要用“符合 OCP”取代回归验证。
  2. 若税额改为每行分别舍入后汇总,现有 invoice 的“先求总净额再计算税”不再足够。写两条会导致舍入差异的合成订单行和可失败的税额断言,指出必须修改哪段计算与结果合同;无需预先创建一个税法规则引擎。

参考与系列导航