重构越改越乱?先固化边界再动内部结构

重构常常开始得很体面——“把这段逻辑抽出来”“顺手统一一下命名”,但改到第三天,你会发现改动像藤蔓一样蔓延到十几个文件,测试挂了,需求还得照常上线,最后只能一边回滚一边打补丁。问题通常不在重构本身,而在于动手之前没有把边界固定下来。

重构的定义决定了边界的重要性

重构有个严格定义:在不改变外部可观察行为的前提下,调整内部结构。注意这句话里有两个约束——“不改变外部行为”是底线,“内部结构”是操作范围。凡是会改变接口、语义或数据格式的改动,本质上就不是重构,而是功能变更,必须单独走流程。

分不清这两类改动,是重构失控的第一大原因。真正高效的重构,是让重构与功能开发像两条并行却互不干扰的轨道。

先给重构划定三层边界

  1. 行为边界:哪些接口、哪些返回值、哪些副作用必须 100% 保持一致。把这些列出来,作为不可动摇的验收条件。
  2. 代码边界:这次改哪些模块、哪些文件。明确写下来,遇到边界外的代码,先记录不动手,避免范围蔓延。
  3. 风险分层:把要改的东西按风险分成核心层、关键层和外围层。核心层人工主导、小步验证;外围层可以更激进一些。同样是重构,不同区域的谨慎程度不该一样。

把测试当成安全网,而不是负担

重构最容易翻车的地方,是“没有测试就动手”。在改动之前,先给要动的代码补上能描述现有行为的测试——哪怕这些测试固化的是一些看起来“不太对”的行为,那也是你后续判断有没有改坏的基准线。

一个实用做法:先补测试,跑通,再提交一个“只加测试不改代码”的变更;然后才开始重构。这样一旦重构出问题,你能立刻区分是测试写错,还是代码改坏。

小步提交,让每一步都可回退

重构的节奏比幅度更重要。每次提交只做一类改动——这一步只做重命名,下一步只做函数抽取,再下一步只做条件反转。每步都要能通过测试、能编译、能被 review。这样即便某一步出问题,回退的范围也只是一小步,而不是整个下午的工作。

经验法则是:让代码始终处于可工作状态。任何一次提交之后,如果被紧急要求上线,都应该能直接发出去。

一个案例

某团队要重构一个跑了多年的订单模块。第一次尝试是让开发“边重构边加新功能”,结果两周后代码更难懂了,缺陷率上升,功能还延期。第二次他们换了方式:先给订单创建、金额计算、状态流转三个核心链路补上回归测试(覆盖率从不足 20% 提到 70%),再明确列出“只改内部结构、不改接口”的边界,按依赖关系从最底层的小函数开始,一次提交只做一种重构手法。三周后,模块行数减少了两成,改动期间没有产生线上故障。

常见误区

把重构当重写。一次性推倒重来风险极高,尤其是还在提供业务价值的系统。小步累积往往更快也更安全。

没有目标就动手。“看着不顺眼”不是理由。清楚写出要解决的问题——是理解成本高,还是每次改都出 bug,目标不同手法也不同。

重构和功能变更混在一次提交里。Review 的人分不清哪些是行为变更、哪些是结构调整,出问题也难以定位。

忽略技术债的代价。技术债的利息是不稳定性和不断攀升的维护成本,拖延只会让未来的重构更贵。但也要接受:不是所有债都值得现在还。

行动建议

下次想动手改代码前,先花二十分钟写三行字:我要解决的痛点是什么、这次的行为边界在哪里、代码边界在哪里。然后补上第一批测试,第一次提交只加测试。把重构拆成能被打断、能被回退的小步,你会发现它不再是件让人害怕的事。

标签:#, #, #