设计模式 E08:遗留代码的小步重构
没有测试的遗留代码,第一步不是套模式,而是把现有行为钉住。特征测试不代表旧行为都正确,它只说明这些输出不能在重构时无声改变。接缝建立以后,再把兼容行为、已知缺陷和确认后的变化分开。
特征测试先冻结可观察行为
labs/E08 的 LegacyCheckout.checkout 解析 sku:cents:state。开放订单返回九折收据,被封禁返回 DENIED,解析失败返回 ERROR。特征测试覆盖成功、封禁、未知状态、负金额、最大 int 金额、空 SKU、字段缺失、金额解析失败和溢出解析失败。测试还记录一个已知缺陷:空 SKU :100:open 返回 OK::90。记录它的目的,是防止重构时顺手改变外部可见行为。
1 | |
这组测试是刹车。后续移动代码、提取函数、引入接口时,只要兼容合同被破坏,就能立刻看到失败。它不替需求决策背书,也不阻止后续修 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。
- 给旧入口增加“金额不是数字”的特征测试,再建立 parser 接缝;不要在同一步修改错误文本。
- 把空 SKU 修复拆成两次提交思路:一次保持旧行为并建立接缝,一次增加确认变化断言并改实现。写出每一步应运行的测试命令。
参考资料:
- Michael Feathers:Working Effectively with Legacy Code、Working Effectively With Legacy Code 论文。
- Martin Fowler:Legacy Seam、Definition Of Refactoring。
- 本地实验:
examples/design-patterns/labs/E08/与examples/design-patterns/evidence/E08/RUN.md。
上一节:E07 真实库里能看到哪些模式;下一步:回到 00 可工作的报价为何难修改,用完整系列重新检查一个变化需求。






