我为什么开始反对过度重构:一次凌晨三点的线上事故教会我的事

🔑 关键词:过度重构,技术债,代码维护,软件工程实践,重构代价

📖 摘要:基于一次真实线上事故,深入剖析过度重构的隐性成本,对比重构收益与风险,提出工程判断的新视角。

起因:一次看似完美的重构

图片

上个月,我负责的一个订单服务模块经过了整整三周的“精心重构”。代码从原来2300行缩减到1400行,圈复杂度从平均12降到6,测试覆盖率从57%提升到89%。一切指标都在向好的方向狂奔,代码审查一次通过,同事在评论里写了“漂亮”两个字。我甚至把重构总结写进了团队周报,配上那两张对比图,觉得自己是真正的工程师。

可就在重构上线后的第5天凌晨2点47分,PagerDuty把正在做梦的我吵醒。线上出现了一堆诡异的超时告警,定位后发现是重构时我顺手把缓存更新策略从“先更新数据库再删缓存”改成了“先删缓存再异步更新数据库”。当时觉得这样更符合最终一致性,而且我特意加了重试队列。但我忽略了原系统里一个隐藏的幂等依赖:旧逻辑里删缓存前会读一次DB,从而天然减少并发写冲突。重构后这个边界消失了,在高并发下直接导致缓存击穿和雪崩。

图片

对比:看似公平的天平其实缺了一块

如果只看代码指标,我的重构完胜。但软件工程里有一项成本从来不会出现在任何IDE的统计面板上,那就是历史行为的时间积累。旧代码那2400行里,有76行是处理各种边界情况的。它们中有的是因为第三方的接口返回值偶尔会多一个空格,有的是因为某个老客户还在用不支持新时间格式的旧协议,这些逻辑不是冗余,而是用事故和工单换来的“行为化石”。重构时我一眼看上去觉得它们“丑”,“不符合现代写法”,就自作主张套用了新的策略模式。

图片

后来我把所有边界情况列出来,逐个对比前后行为,才发现至少有8个细微差异是原代码里刻意保留的。我为了“整洁”付出的代价,是让整个团队花了两天时间去review那76行化石背后的商业规则,并且不得不把其中5个差异作为新兼容层重新加回去。也就是说,我的重构实际上是“先拆坏再补上”,而中间的窗口期还在线上真实运行了五天。你问我技术债?旧代码的维护成本确实高,但过度重构创造了一种新债务——“虚假整洁债”:表面上结构美了,实际却抹掉了系统对混乱现实的全部记忆。

独立视角:重构应该是一门“考古学”而不是“美学”

图片

我在四家公司待过,见过至少上百次重构。大部分成功的重构都有一个共同点:不是基于“这代码太难看了”而是基于“这个模块下周就要加一个新功能,现有结构让改动成本超过一天”。换句话说,有效的重构是被业务需求推着走的,而不是被工程师的洁癖拉着跑。如果没人催你“本周加个字段”“下月接个新渠道”,那你那个模块的重构风险就远大于收益,因为你在用确定性的投入去交换不确定性的未来。

图片

更吓人的是,工程师往往只计算重构的开发时间,而忽略认知切换成本。那次事故后我统计了一下,整个事件从发现到恢复花了3小时15分钟,加上后续加固和回滚补丁,总共磨掉了每人接近10个小时。这还没算团队成员因为半夜被叫醒导致第二天效率下降,以及客户那边支付的信任损失。如果把这笔账算上,我那三周的重构总成本已经是原计划估算的4.7倍,而所谓的受益,只是让一个没有业务变更的模块看起来更像教科书。

结论:给你的重构清单加三行字

图片

我并没有从此不做重构。相反,我现在更积极地做小步重构——每次只改一个行为,PR不超过300行,并且强制要求把每个行为差异用注释写下来。但我给自己的流程加了三行硬标准,你可以抄走:第一,如果找不到一个“未来的业务原因”来解释这次重构,那就先不做;第二,凡是涉及缓存、队列、分布式锁这类有时间语义的代码,重构前必须画一遍时序图,把旧图和新图重叠对比;第三,如果旧代码里有你自己也看不懂的注释,那大概率是一个“化石”。先去找作者问清楚再动手,而不是直接删掉。

最后补一句,软件工程里没有绝对美丑,只有历史和环境。代码整洁是个好目标,但别让它变成一种傲慢。因为总有一天,会有一位凌晨三点被手机震醒的倒霉蛋,去替你偿还那份“看起来更整洁”的代价。而我,只是希望那个倒霉蛋不是你。