先说说那个让我翻车的重构
去年冬天,我接手了一个跑了四年的老服务。技术栈还是Spring Boot 1.5 + 一个自研的ORM,数据库表有80多张,最核心的那张订单表有97个字段。代码里最让我受不了的是一个快3000行的OrderServiceImpl,里面塞了各种状态流转、优惠计算、库存扣减、甚至是发送短信的代码。我看完第一反应是:这堆屎山我必须给它铲平。跟产品经理拍胸脯说给我三周,我把这模块重写一遍,保证更清晰、更快、后续加需求容易十倍。
结果呢?三周重构完,联调了四天。上线第一天就出了事——有个老订单的状态历史记在另一张表里,我新设计的状态机没考虑到一种“订单已取消但退款未处理”的中间态,线上积压了2000多单通知商家失败。那个晚上我蹲在机房里,盯着监控面板上的红色告警,恨不得穿越回去把之前的自己掐死。
后来我跟一个在阿里待了八年的老工程师聊这事,他说了一句让我记到现在的话:“重构最大的风险不是你会不会写新代码,而是你还没有完全理解旧代码为什么长成那样。” 我当时只看到旧代码丑、乱、重复,却没想明白这些丑陋的背后有多少是业务临时妥协、多少是历史数据遗留、多少是当年某个小概率bug的补丁。
对比一下我的“屎山” vs 我的“优雅”
重构前我用splint工具扫过一次,OrderServiceImpl里有12个if-else嵌套超过4层,循环里查数据库有18处。我重构的时候,把状态流转单独抽了一个OrderStateMachine,用策略模式替换了那一大堆switch-case,还把公共的数据访问都收敛到repository层。代码量从3200行降到了1400行,单元测试从8个增加到47个,覆盖率从22%提到了68%。我一度觉得自己帅呆了。
但现实是,那47个测试全是我基于“我以为的业务逻辑”写的。旧代码里有个字段叫special_flag,我查了所有引用它的地方,发现它只在一种已经下线的营销活动里会被置为1。我自作聪明地在重构时把这个字段的赋值逻辑删了,只保留了读取。结果线上有个2019年下单的老用户,他的订单正好卡在活动下线前,special_flag在数据库里是1,但新代码里根本没人会判断它了——于是这个订单的优惠分摊算法走岔了路,退了两次差价。
后来我做了个对比:旧代码虽然乱,但几乎所有逻辑都是“线性”写下来的,你跟着一个订单从下单到完成,整个流程就是从上往下读一遍。新代码我拆得很漂亮,有状态机、有多个策略实现类、有事件总线。数据流是分叉的、异步的,调试时需要同时记住六个不同的类。旧的烂,烂在表面上;新的乱,乱在骨子里——因为真实业务是线性的、强时序的,硬套上这些“优雅”的模式,等于给跑车装了履带。
我后来是怎么处理“重构”这件事的
那之后我给自己定了三条铁律。第一,如果只是觉得代码丑,不碰;只有当你需要在同一个地方连续加第三个功能,并且这个功能跟旧逻辑纠缠不清的时候,才去局部重构。第二,重构前必须从线上拉足够的真实样本,把所有能跑的历史订单、历史请求都跑一遍新逻辑,对比输出差异。别信什么测试覆盖率,我的47个测试还没一个线上回放抓的问题多。第三,重构时永远保留一条“后退路线”——不是代码回滚,而是把新代码在旧逻辑旁边跑灰度,哪怕多花两周部署时间,也比出事强。
现在再有人跟我说“这代码太垃圾了我们要重构”,我就回他:“你先给我讲清楚从上个月到现在这模块里所有字段的含义,包括那个叫remark的字段为什么既有商品备注又有投诉备注。” 很多时候对方讲一半就卡壳了,这时候大概率说明他也没搞懂,那咱们就别谈重构了,先把能删的注释删干净吧。
顺便说个数据点:我后来统计过,那个项目过去一年总共改了47次需求,真正需要动核心状态流转的只有3次,其他都是加参数、加分支、改文案。所以,把if-else换成状态机并不会让这些简单需求变得更简单,反而每个新需求都要多改一个地方。大多数项目里的“复杂”,是业务本身在边缘情况上的不可消解,不是代码被写坏了。
现在我会跟团队说,最重要的不是把代码重构得多干净,而是让每个人都能在一个小时内定位到一个问题的入口。哪怕这个入口是那个3000行的方法,只要能通过find搜索到,就没那么可怕。真正的重构不是把烂代码变成好代码,而是把一个你不理解的烂代码,变成一个你理解的烂代码——然后你才有资格决定哪部分值得动。