我这辈子提过五次重写,成功两次,失败两次,还有一次到今天也说不清算成功还是失败。这篇就讲这几件事,每件都有具体的数字和后果。
第一次:赢了,但赢得很险
那是一个订单状态机,最早是用一堆 if-else 写的,我接手时有 1400 行,17 个状态,我数出来 63 条状态转移分支。改一个需求平均要动 5 个地方,漏改一处线上就出脏数据。
我花了三周重写成显式的状态机表:一张二维表定义合法转移,加一个执行器。新代码 380 行。
关键在于我做了两件事保命:
- 新旧两套代码并行跑了两周,每笔订单都算两遍,结果不一致就打日志但仍以旧逻辑为准。两周里抓到 47 次不一致,其中 41 次是我的新代码错了。
- 那 63 条分支我全部写了对照测试,是从生产日志里抽的真实数据。
如果没有影子运行,我大概率会在上线第一天炸掉。
第二次:输了,输得很惨
一个内部的数据同步服务,Python 写的,我坚信应该换成静态类型的语言,理由是「运行时错误太多」。
我立了个项,估两个月。实际做了五个月,中途上线了两次又回滚两次。最后砍掉了。
失败的原因我事后想得很清楚:我重写的是形式,不是本质。那个服务真正的复杂度在于它对接了 9 个上游数据源,每个源都有自己的怪脾气——有一个源会在月末返回多余的空行,有一个源的时间戳是本地时区但不带标记,有一个源在数据缺失时返回字符串 NULL 而不是空。
这些知识全部散落在原代码的各种奇怪判断里,没有文档,没有注释。我重写的时候把那些「看起来很脏」的分支当垃圾清掉了,结果每清掉一个就在生产上还回来一个 bug。
老代码里那些莫名其妙的分支,往往是用事故换来的。你看不懂它,不代表它没有理由。
第三次:说不清的一次
一个前端管理后台,从旧框架迁到新框架。做了四个月,功能一比一还原。上线后用户体感几乎没变化,性能提升大概 15%。
从业务角度看,这四个月产出为零。从工程角度看,之前每次招人都要花两周教旧框架,之后新人第一天就能提 PR,而且我们两年里招了六个人。
我现在的看法是这次重写是对的,但当时我没法证明它对,只能凭直觉硬顶。这种事很难有标准答案。
我现在的四个自检问题
被坑过之后,每次想重写我都先回答这四个问题,答不上来就不动:
- 我能列出老代码里至少 10 个我看不懂的地方吗?如果一个都没有,说明我根本没读懂它。如果列出来了,我必须逐个搞清楚为什么存在。
- 有没有办法把重写切成能独立上线的小块?能切就切。不能切的重写,风险呈指数级上升。
- 能不能做影子运行或双写对比?没有对比手段,就相当于闭着眼开车。
- 如果重写失败,回滚代价是什么?数据结构变了的话,往往回不去。这时候需要格外谨慎。
一个更根本的观察
我发现自己想重写的冲动,绝大多数时候来自于「读代码很痛苦」这个感受,而不是「这段代码有具体的问题」。
这两者的区别非常大。读代码痛苦是我的问题,代码有问题才是代码的问题。分辨的方法是逼自己写下三个具体的、可度量的痛点:比如「改一个字段需要动 6 个文件」「新人上手平均 3 周」「这个模块过去半年出了 8 次线上问题」。
写得出来,重写有底气。写不出来,那我大概只是想找个理由重新写一遍我自己喜欢的风格。
顺带说,第二次失败之后我做了什么?我花两周给那个 Python 服务补了 130 个测试和一份 3000 字的《上游数据怪癖清单》。之后两年它稳定运行,我们只改过 4 次。有时候最好的重写就是不重写。