没有测试的遗留代码,第一步不是套模式,而是把现有行为钉住。特征测试不代表旧行为都正确,它只说明这些输出不能在重构时无声改变。接缝建立以后,再把兼容行为、已知缺陷和确认后的变化分开。

特征测试先冻结可观察行为

labs/E08 的 LegacyCheckout.checkout 解析 sku:cents:state。开放订单返回九折收据,被封禁返回 DENIED,解析失败返回 ERROR。特征测试覆盖成功、封禁、未知状态、负金额、最大 int 金额、空 SKU、字段缺失、金额解析失败和溢出解析失败。测试还记录一个已知缺陷:空 SKU :100:open 返回 OK::90。记录它的目的,是防止重构时顺手改变外部可见行为。

1
2
3
4
5
6
assertEquals("OK:paper:90", legacy.checkout("paper:100:open"));
assertEquals("DENIED", legacy.checkout("paper:100:blocked"));
assertEquals("ERROR", legacy.checkout("bad"));
assertEquals("ERROR", legacy.checkout("paper:not-number:open"));
assertEquals("ERROR", legacy.checkout("paper:2147483648:open"));
assertEquals("OK::90", legacy.checkout(":100:open"));

这组测试是刹车。后续移动代码、提取函数、引入接口时,只要兼容合同被破坏,就能立刻看到失败。它不替需求决策背书,也不阻止后续修 bug。

接缝只做替换,不顺手修需求

RefactoredCheckout 把支付调用抽成 PaymentPort。它仍保留旧解析、未知状态、负金额、最大 int 金额和空 SKU 兼容行为;字段缺失、数字解析失败、溢出解析失败仍返回 ERROR。测试还记录接缝副作用:只有旧实现会返回 OK 的输入才调用支付端口,调用参数与旧收据里的 SKU、折扣金额一致;封禁和解析失败不调用支付。接缝的目的只是让外部协作可替换,不把业务规则顺手改掉。

兼容接缝阶段只移动依赖边,不改变旧输出:

flowchart LR
    Test[特征测试] --> Legacy[LegacyCheckout]
    Test --> Refactored[RefactoredCheckout]
    Refactored --> Port[PaymentPort]
    Port --> Fake[测试替身]
    Legacy -.旧输出 parity.-> Refactored

ConfirmedFixCheckout 才拒绝空 SKU,并把支付失败返回 PAYMENT_FAILED。这两个变化是本章为了演示“兼容重构之后再改需求”而新增的教学要求,不代表真实用户已经确认了这些业务规则。代码评审能清楚地区分:哪些是兼容重构,哪些是后续需求变化。

flowchart LR
    Refactored[兼容重构] --> BlankSku[教学新增需求: 空 SKU 拒绝]
    Refactored --> PaymentFailure[教学新增需求: 支付失败显式返回]
    BlankSku --> ConfirmedFix[ConfirmedFixCheckout]
    PaymentFailure --> ConfirmedFix
行为 Legacy Refactored ConfirmedFix
paper:100:open OK:paper:90 保持 按支付结果
paper:100:blocked DENIED 保持且不支付 保持
paper:100:pending OK:paper:90 保持且支付 按支付结果
paper:-10:open OK:paper:-9 保持且支付 按支付结果
paper:2147483647:open OK:paper:1932735282 保持且支付 按支付结果
bad / 字段缺失 ERROR 保持且不支付 未作为兼容合同
paper:not-number:open / 溢出金额 ERROR 保持且不支付 未作为兼容合同
:100:open OK::90 已知缺陷 保持且支付 教学新增要求:拒绝

小步重构先保护行为,再引入接缝,最后改确认过的需求。把三步混在一起,失败时无法判断是重构破坏还是需求变化。

验证与练习

运行 ./mvnw -B -ntp -pl labs/E08 -am test;LegacyRefactoringTest 通过 4 个测试。原始日志在 examples/design-patterns/evidence/E08/targeted.stdout.txt。真实支付、生产流量回放、大范围重构性能 NOT_RUN。

  1. 给旧入口增加“金额不是数字”的特征测试,再建立 parser 接缝;不要在同一步修改错误文本。
  2. 把空 SKU 修复拆成两次提交思路:一次保持旧行为并建立接缝,一次增加确认变化断言并改实现。写出每一步应运行的测试命令。

参考资料:

上一节:E07 真实库里能看到哪些模式;下一步:回到 00 可工作的报价为何难修改,用完整系列重新检查一个变化需求。