重构之前先量三组数据:决定动不动手的硬指标

一个电商团队花两周重写了订单模块,上线后第一周故障率不降反升,最后只能回滚到旧实现。事后复盘最扎心的一点是:他们动手前没有量过任何一个指标,只是觉得那段代码「很难看」。

重构不是审美活动。它是一次投资决策,判断依据应该是数据,而不是直觉。

第一组数据:变更频率与缺陷密度

值得动的代码不是最丑的,而是最贵的。业内把这种判断叫热点分析:把版本历史里每个文件的改动次数和它的复杂度叠在一起看,落在右上角的那一小撮文件,就是真正消耗团队预算的地方。

操作上并不复杂。用 git log –numstat 统计近半年的提交次数,再和缺陷单、线上告警单做一次关联。假设某个文件半年被改了 40 次,贡献了 12 个线上问题,那它的问题就不是命名风格,而是设计承载不住需求变化。

反过来看,一个三年没人碰过的遗留模块,写得再难看,动它的期望收益也接近零,风险却是实打实的。

第二组数据:圈复杂度的绝对值和分布

圈复杂度(McCabe,1976)数的是控制流里的独立路径数。顺序执行的代码复杂度为 1,每多一个 if、case、while、catch,就多一条路径。可以用 V(G)=E-N+2 计算,也可以直接数判定节点再加一。

这个指标真正衡量的是测试成本。复杂度 25 的方法,理论上需要至少 25 条用例才能覆盖全部路径,而多数团队的实际覆盖远低于此,于是每一次修改都在赌没被覆盖的那几条分支。

实践中的参考线大致是:单方法控制在 10 以内,超过 15 列入观察名单,超过 30 基本无法安全修改。要注意的是它和行数不成正比——一个 200 行的线性批处理可能只有 3,一个 60 行的多层嵌套校验却可能到 18。

第三组数据:测试护栏和理解耗时

前两组数据决定值不值得动,第三组决定能不能安全地动。

有个土办法很有效:找一位没接触过该模块的同事,让他读代码并回答「改这个字段会影响哪些流程」,记录他用时多久、答对多少。如果超过 20 分钟还理不清调用链,说明这个模块的认知成本已经失控。

同时看测试:关键分支有没有自动化用例兜住?没有护栏的重构不叫重构,叫偷偷重写。

四类情况的处理顺序

  • 变更频繁 + 缺陷多 + 复杂度大于 15:优先重构,但必须拆成可回滚的小步
  • 变更频繁但复杂度正常:多半是领域模型的问题,先调结构再动实现
  • 变更极少但复杂度极高:登记下来,等它第一次进入需求范围时再处理
  • 数据都正常、只是命名难看:顺手改掉,不需要立项

三个很容易踩进去的坑

第一个坑是把重构做成重写。重写的风险曲线在第一周之后会陡增:新代码带来新缺陷,旧代码里那些没写下来的隐性知识却已经丢掉了。正确做法是保持对外行为不变,一次只做一类改动。

第二个坑是把行数当复杂度用。三千行的配置文件可能完全无害,一百五十行的调度逻辑才真的致命。

第三个坑是护栏没建好就开拆。缺回归用例的时候强行拆分,等于把线上环境当测试环境用。

还有一个常被忽略的维度:这次改动可逆吗

同样复杂的两个模块,改动风险并不一样。判断标准很实在:如果上线之后发现不对,多久能退回去。

十分钟内能回滚的,可以大胆动;涉及数据结构变更、缓存格式调整、对外接口语义变化的,就得换一种做法——先让新旧两种格式并存,双写双读,观察一个完整的发布周期,确认没有遗漏的调用方之后,再清理旧逻辑。

这里有一条很省心的纪律:把不可逆的操作留到最后。先把可回滚的部分全部做完并验证,再执行那一步唯一的、不可逆的动作,并且在此之前做一次备份和数据核对。

度量口径也要统一。不同的人用不同的插件统计复杂度,结论可能完全对不上。团队里最好固定一套统计脚本、固定统计周期,比如统一看近半年的提交历史,否则讨论很容易滑向「我觉得这段更乱」这种没有结论的争论。

下一个迭代可以落地的动作

三件事按顺序做:先跑一次提交统计,列出改动最频繁的十个文件;再给这十个文件按圈复杂度排序;最后挑出复杂度最高、且下个迭代恰好要改的那一个,补上三条关键路径的单元测试,然后按「提取方法—内联简化—调整命名」的顺序做小步提交,每步之后跑一次测试,绿灯才继续。

数据不会替你下决心,但它能让你在被问「为什么要动这块」时,给出一个不靠感觉的回答。

标签:#, #, #