Legacy代码重构失败的主因是行为变更未被文档化,提出用表征测试+变异检查+小diff的组合在写代码前验证现有行为。
遗留代码的重构很少因为新代码崩溃而失败。它们失败是因为你改动了某个没有人记录过的行为。在写第一行新代码之前,你就能发现这个问题。
这个预检流程需要二十分钟。它使用表征测试、变异检查和一个小型 diff。不追求理论上的纯粹,只是一个可重复的工作流。
盲目搜索替换式的重构有一个隐藏的风险:调用方。每个导出的函数都可能向某个隐藏系统输送数据。更改返回值类型可能在凌晨三点影响生产。
预检让契约变得可见。它回答的是:"这个函数现在实际上做什么?"然后你才能安全地改动它。
从你想要重构的函数开始。列出仓库中每个调用者。使用这些命令:
grep -rn "processOrder(" src/
rg "processOrder" tests/
统计调用者数量。记下函数签名和返回类型。这就是你的契约。
Callers found: 4
- src/payments.js:12
- src/reports.js:77
- src/admin.js:3
- tests/integration.test.ts:88
现在你知道表面变化时哪些代码会出问题。
编写测试来记录当前行为,而不是预期行为。使用测试或保存的生产日志中的真实输入。
test("current behavior: processOrder sorts by date", () => {
expect(processOrder(inputFixture)).toEqual(expectedFromCurrentCode);
});
fixture 应该包含边界情况:null、空集合、重复 ID。不要对"正确性"做断言。只对当下做断言。
添加第二个测试来检查返回结构。检查字段,而不只是值。
test("current shape: processOrder returns an array", () => {
expect(Array.isArray(processOrder(inputFixture))).toBe(true);
});
这些测试是镜子。它们映射代码,而不是文档。
表征测试可能是假的。它们可能在行为变化时仍然通过。变异测试解决了这个问题。
对函数引入一个小的故意变更。例如,反转一个 if 条件或删除一行。
if (order.total < 0) { // mutated from > 0
运行测试。如果它们失败了,你有安全网。如果它们通过了,你的测试太弱了。添加更多断言直到变异体被杀死。
npx test
> 1 failed, 1 passed
# the mutant was killed
用另一个变异重复。移动一个变量,删除一个默认值,重排序一个循环。每个被杀死的变异体都证明你的测试能嗅到变化。
不要重构整个函数。选择一个分支或一条语句。把它移到一个新的辅助函数中或给它重命名。在每个微步骤后运行测试。
npm test && npx mutation-check ./src/order.js
然后提交。如果某个步骤失败了,回滚那一步。你仍然有一个可用的基准。
每个切片的提交创建了干净的历史。你的审查者看到的是逻辑序列,而不是一个巨大的 diff。
在关闭 ticket 之前,比较新旧函数的调用者。你改了签名吗?返回结构呢?错误行为呢?
公共接口的 diff 捕获隐藏的耦合。使用 git diff:
git diff HEAD~1 -- src/order.ts
查找被删除的导出、变更的参数顺序或新的异常抛出。任何一种都会悄无声息地破坏调用者。
在预检中使用全部三个。每个覆盖不同的失败模式。
你可以完全不花一分钱运行整个预检。测试运行器是免费的。变异检查可以作为脚本运行。diff 是原生的 Git。
如果你想让预检步骤生成为仓库特定的检查清单,MonkeyCode 的免费模型访问可以生成一个。免费服务器选项让你无需搭建自己的基础设施就能运行这个流程。披露:本文是作为 MonkeyCode 产品推广的一部分准备的。
这是工具唯一出现的地方。这个工作流本身是属于你的。
变异测试不能证明正确性。它证明你的测试能检测特定变更。有些变异版本可能因为等价代码而通过。审查每个存活的变异体。
二十分钟的时间盒假设函数较小。大型服务需要更多准备。另外,表征测试会冻结 bug。如果当前行为是错误的,测试会钉住那个错误。
遗留代码的初学者:这个工作流需要熟练使用 git 和测试运行器。如果你仍在学习重构,先打印清晰的图表。
紧急热修复时也要避免。预检需要的时间在故障期间你可能没有。使用回滚代替。
当你有一个预言机时,重构不是猜测。预言机就是当前行为。在测试中捕获它,变异来证明它,然后改变一小片。二十分钟的预检防止二十小时的回滚。
这就是全部策略。它很无聊。但它有效。