会员满 100 元送礼,订单满 100 元包邮。这两个判断看起来相同,于是有人把门槛抽到 eligible(int cents)。现在会员部门把送礼门槛改成 150 元,物流部门维持 100 元。只改共享常量会同时改变包邮;保留旧常量又会错误地送礼。这里的相似代码并不代表同一条业务知识。

先让独立变化暴露出来

Before 的两种优惠都走同一个 eligible;测试特意断言它在 120 元时仍然送礼,用来记录旧实现的错误,而不是把错误算成新需求通过。最小修复 After 并没有引入策略接口:保留两处比较,礼品条件改为 >= 15_000 分,运费条件保持 >= 10_000 分。金额用分表示以免例子引入浮点舍入问题,这不是通用货币模型。

1
2
3
4
5
6
7
public boolean loyaltyGift(int cents) {
return cents >= 15_000;
}

public boolean freeShipping(int cents) {
return cents >= 10_000;
}

对调用者而言,两条分支都能回答同一个旧问题:20_000 分时均成立;新问题却要求 12_000 分时只有包邮成立。Alternative 把两个条件放进同一个 switch (benefit):当入口只有一个、优惠种类固定时,这样也足够,不必为了让代码看起来“可扩展”增加注册表或抽象工厂。未知优惠名显式抛异常,不隐式将其视为不符合资格。

设计 12_000 分时送礼 / 包邮 再改送礼门槛时改哪里
Before 是 / 是(不符合新增要求) 共用门槛会波及物流
After 否 / 是 loyaltyGift,物流判断不动
Alternative 否 / 是 gift 分支,条件仍集中在一个类

DRY 要防止同一事实以多个副本维护,不能从两行字面相似直接推出共享的变化理由。KISS 在这里是先写两个能独立改变的判断;YAGNI 则要求在实际需求出现前保持克制:尚无第三种优惠或运行时可替换需求时,不要预置接口、工厂和插件。它们不是禁止抽象的定律:若两个入口确实受同一门槛约束,并有共同变更证据,共享一个命名规则更可能减少遗漏。

flowchart LR
    BeforeGift[Before.loyaltyGift] --> Shared[eligible 10000]
    BeforeShipping[Before.freeShipping] --> Shared
    AfterGift[After.loyaltyGift 15000]
    AfterShipping[After.freeShipping 10000]

实验如何判定

在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/13 -am test,随后运行 ./mvnw -B -ntp verify。本章四项断言覆盖旧合同、旧实现的反例、新边界 14_999/15_000 分、未知优惠;本次真实命令、JDK、退出码和原始输出记录在 examples/design-patterns/evidence/13/RUN.md。这些测试既没有证明数据库金额的一致性,也没有覆盖负数输入的业务处理;负数是否允许应先由真实入口合同决定,不能从本章示例推断。

练习:

  1. 新增“积分礼品门槛 180 元,运费仍为 100 元”的变化。先写 100/150/180 元边界断言,再核查旧包邮合同是否仍成立,并记录三个方案各需修改的位置。
  2. 假设物流与会员确认门槛在未来由同一个合约统一发布。设计能证明“同一事实”的测试;比较保留两处分支与共享一个值对象,指出重新抽象何时才值得。

参考资料

  • Martin Fowler, Yagni。
  • Andy Hunt 与 Dave Thomas, The Pragmatic Programmer, 20th Anniversary Edition 官方公开样章 DRY—The Evils of Duplication:DRY 定义为每一份知识在系统内只有单一、明确、权威的表示。
  • NASA/NTRS, KRUSTY Reactor Design:在 KRUSTY 目标取舍中明确使用 “Keep It Simple, Stupid (KISS)” 原则;NASA Appendix C: How to Write a Good Requirement 的需求校验清单也要求需求 concise and simple。
  • 来源边界:writing-plans/design-patterns/SOURCES.md。
  • 实验代码:examples/design-patterns/labs/13/。

上一节:12 谁该决定库存预留;下一节:14 包依赖为什么成环。