代码重构的5条军规:避免重写陷阱,小步快跑才是关键

代码重构的5条军规:避免重写陷阱,小步快跑才是关键 我刚工作第三年的时候接手过一个支付对账模块那代码叫一个酸爽——三千多行挤在一个文件里函数之间互相调用变量名从 a1 排到 a99注释基本等于没有。当时我满腔热血要重构领导说行给你两周。结果我花了两周写的重构代码上线第一天就出了事故回滚、被骂差点走人。后来我才想明白不是重构这件事错了是我对重构的理解太浅。我把“重构”当成了“重写”把“代码优化”当成了“炫技表演”。这几年我陆陆续续主导过几十次规模不一的重构从几十行的函数清理到整个模块的重新分层慢慢总结出了 5 条军规。靠着这些原则我后来在某次核心系统的重构项目中站稳了脚跟项目上线稳定运行老板也痛快地给我涨了 30% 的薪资。这篇文章把这些原则连同我的实操流程、踩坑记录一起分享出来希望能给你一些参考。1. 重构这件事别轻举妄动——先搞清楚“为什么”和“值不值”很多程序员对重构有一种本能的冲动看到烂代码就想动手看到重复逻辑就手痒。这个心情我特别理解但重构不是请客吃饭更不是代码洁癖的自我满足。在动手之前必须先回答三个问题为什么要重构、现在是不是重构的时机、重构的投入产出比是不是划算。1.1 重构的起点不是“看代码不爽”而是痛得实在受不了了我这些年观察下来真正值得重构的信号其实很明确改一个功能要动七八个文件而且每次改动都牵扯出一堆隐藏依赖。测试成本越来越高跑一遍核心流程要半天很多逻辑没法自动化验证。“改 A 坏 B”频繁出现团队每天光是在线上救火就耗掉大量精力。新人上手极慢熟悉业务代码的时间比熟悉业务本身还长。代码腐化速度快每次迭代都在给现有结构增加补丁越补越烂。只有当你或者你的团队在日常迭代中实实在在感受到了这种“痛”重构才有足够的动力和目标。如果代码虽然丑但稳定运行、改动频率低、团队的维护成本可控那它就属于“可以不动”的状态。这时候强行重构反而是在给项目增加无谓的风险。1.2 哪些情况下绝对不要碰重构比“什么时候该重构”更重要的是“什么时候不该重构”。我吃过亏所以把这些场景专门列出来禁止重构的场景原因大版本发布前两周风险窗口太窄一旦出问题没有弥补时间没有自动化测试覆盖的老模块没有任何保护网改错一个细节就可能全盘崩溃团队里没人熟悉这块业务逻辑重构的前提是“搞懂了”搞不懂就动手等于盲人摸象代码马上要被替换淘汰投资没有回报纯属白费力气心情不好想靠重构“放松”重构是高强度的脑力劳动情绪不稳定时最容易出事记住一个核心判断标准重构的收益必须大于风险。收益是隐性的、长期的风险却是显性的、即时的。如果连自己都说服不了这场重构值得做那大概率就是不该做。2. 军规一测试绿了再动手没有保护网的重构都是裸奔我那次支付模块翻车最根本的原因就是没有测试保护。当时我觉得自己“人肉测试”就够了逻辑都在脑子里跑通了结果实际上线的时候一个边界条件没处理好直接导致对账差异。从那以后我立了一条铁律没有测试覆盖的代码一律不进行结构性的重构。2.1 为什么测试是重构唯一的“安全带”重构的核心操作是“结构变换”比如搬移函数、提炼新类、改名、修改调用关系。这些变换本身不应该改变系统的外部行为。但问题是你怎么证明“没改坏”光靠“我觉得没问题”是不行的必须有客观的验证手段。测试就是这个验证手段。它在重构前后给你画出一条基线重构前测试全跑通重构后测试仍然全跑通这才说明行为没有被破坏。如果没有这条基线你就等于在悬崖边蒙眼跳舞出事是必然的不出事才是运气好。2.2 补测试不用贪多关键是锁住“当前行为”很多老模块根本没有测试补测试也是一种负担。我的做法是先写特征测试。特征测试Characterization Test的理念很简单——不判断逻辑“对不对”只把当前的输入输出行为完整记录锁定。比如一个订单计算价格的函数你不需要判断它的计价策略是否合理只需要构造一组输入把实际输出记录成断言。这样做的意义在于告诉你“当前系统就是这么跑的”重构之后只要输出对得上就没有改变外部行为。实操步骤一般是梳理这个模块的核心入口函数找出 5-10 个关键业务场景。针对每个场景构造输入数据在重构前把当前代码的实际输出记录下来。把这组输入输出直接固化成自动化测试用例。跑通这组用例绿了之后才开始重构。我一般会先把主链路覆盖住边角逻辑可以后续再补。重构期间每组测试都是你的“心跳”只要变红就说明你刚刚那步操作可能有问题马上停下来检查。3. 军规二小步快跑一次只改一件事为什么“大爆炸式重构”总是翻车因为它把所有变化混在一起一旦出问题你根本无从定位是哪一次改动造成的。人脑能同时处理的变化数量是有限的一次改得越多出错概率是指数级上升的。3.1 大爆炸重构为什么会崩盘想象一下你面前有一座积木搭成的塔你要把它换成一座乐高塔。大爆炸做法是先把积木塔整个推倒然后从零开始搭建乐高塔。问题是推倒的过程中你丢失了原来的结构信息搭建的过程中你又容易遗漏原来的功能细节最后搭出来的塔外表好看但很多内部功能其实已经不对了。我当时重构支付模块就是这种典型的大爆炸做法——花了两周把整个文件推倒重来重新设计了类结构、重写了所有方法。结果就是表面上代码变优雅了但实际上很多边界处理逻辑在重写过程中被遗漏了上线就炸。3.2 小步重构的正确打开方式小步重构的核心原则是每次提交只改变一件事。举几个例子如果这次工作是“给方法改一个更清晰的名字”那就只改名字不顺手调整方法内部的逻辑。如果这次工作是“把一段重复代码提取成公共函数”那就只做提取不顺手优化这段重复代码里的逻辑细节。如果这次工作是“把一个大类拆分成两个小类”那就只做拆分不顺手修改类内部的业务判断。每一步做完之后都要保证代码仍然可以编译、测试仍然可以通过。然后立即提交一个 commit提交信息写清楚“这一步做了什么”。这样一来你的重构历史就是一条一步一个脚印的安全路径任何一步出了问题都可以快速回退。我自己的节奏通常是15 分钟到 2 小时一个变化点。超过这个时间范围还没完成说明这个变化点范围太大了需要拆得更细。4. 军规三重构只动结构不动功能边界画清楚重构最大的诱惑就是“既然代码都打开了顺便把这个 bug 也修了吧”“既然这个逻辑不顺眼顺手调整一下吧”。如果你这么做了你就踩进了重构的头号陷阱混淆“重构”和“功能变更”。4.1 为什么需求、修 bug 和重构必须严格分离我们来分析一个问题重构之后系统出了 bug你怎么判断这个 bug 是重构引入的还是本来就存在的如果你在重构过程中顺手修了一个老 bug又顺手加了一个新判断那排查起来就完全是一笔糊涂账。我在重构项目的过程中曾经在一个类里看到一个明显错误的状态判断当时“顺手”就改了。结果后来这个改动和一个重构步骤叠加产生了一个新的状态歧义排查了两天才定位到。从那次之后我给自己立了一个军规重构进行中发现任何疑似 bug 的地方一律先记录到任务清单等重构完成之后再单独处理。4.2 守住“功能不变”底线的三个技巧技巧一用分支隔离。重构专门开一个分支功能迭代和 bug 修复在另一个分支。两个分支互不干扰最后再统一合并。技巧二定义一份“功能验收清单”。在重构开始前把当前系统最重要的 10-20 个功能行为逐条列出来比如“订单状态流转必须经过 待支付 → 已支付 → 已发货 → 已完成”。重构过程中每条都逐一验证确认重构前后行为一致。技巧三结构变换和逻辑变换严格分开。永远不要在一个步骤里同时做“把方法从 A 类搬到 B 类”和“修改这个方法里的业务判断逻辑”。前者是结构变换后者是逻辑变换一次只做一种。4.3 有些“不太合理”的代码也许正在保护着系统的稳定性重构中经常会遇到一些看起来“不合逻辑”的代码比如多余的判断、看起来永远不会走到的分支、多此一举的空值检查。你可能会觉得它们是垃圾代码想顺手删掉。但请注意很多“多余”的代码是前人为了对付线上某些诡异场景而打的补丁只是没有留下注释。我的做法是看不明白的代码先留着用 git 追溯它的历史提交记录看看当初是为了解决什么问题加上的。如果提交记录也比较模糊那就继续保持原样。重构不是要去理解所有历史债务只是要安全地改善结构。5. 军规四可读性优先别把重构变成炫技现场重构的目标是什么是降低代码的维护成本是让后来人能够快速理解、安心修改。这也就意味着代码的可读性比“优雅”重要得多。5.1 怎么量化“可读性”可读性这东西听起来很虚但其实是可以用一些指标逼近的行人理解一个核心业务函数需要多长时间。重构前如果需要一个下午重构后能不能缩短到半小时。新人接手模块后第一次提交代码需要问多少个问题。代码评审中被提出疑问的点位数量。变量名、函数名是否直接表达了业务含义比如getOrderStatus()显然比getStatus()更清晰更别提getA()这种了。我在重构中特别喜欢做的一步就是把变量名、函数名彻底地、有系统地改一遍。这一步的成本极低但对可读性的提升是立竿见影的。5.2 那些看起来很聪明、实则很坑的写法我在评审代码的时候见过很多“炫技”的情况列举几个典型过度使用设计模式明明一个 if-else 能解决的问题非得抽象出一个 Strategy 接口加四个实现类。结果就是看一段简单逻辑要在五六个类之间来回跳。手写花式函数式链式调用一行代码里套了四五个 map/filter/reduce逻辑确实妙但下一次有人要改其中的判断条件时根本无从下手。短命变量名tmpData、res、data、info这种名字写的时候觉得无所谓读的时候全靠猜。过分精简的三元表达式三元表达式嵌套三元表达式看着高级实际上没人读得懂。提示重构的终极检验标准不是“代码写得漂不漂亮”而是“两周后的自己和刚入职的新同事能不能看懂”。如果你需要写一大段注释来解释这行代码在做什么通常说明这段代码本身就不够清晰。6. 军规五重构成果要可衡量、被看见很多程序员对“向老板汇报重构成果”这件事非常不敏感觉得自己把代码搞得优雅就够了老板看不懂代码自然也就不会认可。但问题是如果老板不理解重构的价值你就很难争取到足够的资源和支持涨薪更是无从谈起。6.1 重构前后的指标对比是技术价值可视化的重要手段我每次重构项目收尾时都会准备一份简洁的重构报告。报告里不写“我重构了哪些类”“我用了什么设计模式”而是写下面这些指标的前后对比指标重构前重构后说明核心模块平均圈复杂度18.57.2复杂度降低改动的出错概率随之降低单元测试覆盖率12%76%覆盖率上来后回归风险大幅下降核心功能平均修复时长6 小时1.5 小时定位问题快修复就快版本上线后一周内线上缺陷数8 个1 个回归风险的直接反馈新成员熟悉模块的时间2 周3 天可读性提升带来的隐性收益这些数据才是老板真正能感知到的价值——业务风险在下降交付效率在提升。6.2 用业务语言讲清楚重构的价值有一次我和老板复盘重构项目我没有说“我把订单模块重构成了充血模型用策略模式替换了状态机”而是说“之前每次改订单状态流转都要动四个文件而且经常改出一个线上问题。现在结构理清了上周一整个迭代只改了 1 个文件上线三天零故障。”老板听完立刻就明白了这个重构的价值。这个沟通方式的核心是把代码层的改动翻译成业务层的语言。不是“我引入了某某设计模式”而是“这个模块出了问题更容易定位了”不是“我消除了重复代码”而是“新增一个优惠券类型只需要改 1 处地方不用再改 5 个文件了”。7. 实战复盘一次可复现的完整重构流程光讲原则还不够我把自己多次使用的一套完整流程整理出来你可以直接套用在自己的项目上。这套流程的核心思路是准备充足、小步推进、验收闭环。7.1 第一步准备阶段约 1 周盘点目标模块圈定要重构的核心类/文件用工具统计圈复杂度、代码行数、依赖关系。补特征测试优先覆盖核心业务链路至少达到主流程 80% 以上行为锁定。确定“当前行为”基线整理出核心功能验收清单。明确目标和边界这次重构要做到什么程度比如“拆分大类”“去除重复逻辑”明确不做什么比如“不优化数据库查询”“不改接口协议”。这一步的关键产出物是一份“我准备动了这是现状”的基线报告。后续每一步都是在这份基线之上做变换确保不越界。7.2 第二步实施阶段2-4 周按模块复杂度可调整按依赖关系排出重构顺序先重构底层工具类再重构领域层最后到接口层。每个模块内部按“提取函数 → 更名 → 搬移 → 拆分”的顺序逐步推进。每完成一个变化点立即运行测试确认全绿后再提交 commit。每天结束时保证代码处于可发布状态。如果你当天做到一半发现有问题宁可回退也不要在半成品状态过夜。我个人的经验是这个阶段最怕的是“越改越上头”。本来只计划重构一个类改着改着发现它的调用方也乱顺手就把调用方也改了。然后发现调用方的调用方也乱……这种扩散式重构会让范围失控一定要靠“验收清单”拉回来。7.3 第三步收尾阶段2-3 天跑一遍完整的功能验收清单确认所有行为与重构前一致。补充必要的文档/注释尤其是那些“历史 obfuscated 逻辑为何存在”的记录。准备重构报告附上前后指标对比、关键改动点、遗留待办。邀请同事做一次代码评审让团队其他人也熟悉新结构。收尾阶段的核心任务是交付闭环。不要做“只写代码不写说明”的重构文档和报告的价值会在后续维护和向上沟通中持续体现。8. 常见问题与排查技巧实录这部分分享几个重构过程中高频出现的问题以及我的处理思路。8.1 重构后功能正常但性能下降了怎么办别慌先定位再优化。很多情况下性能下降是因为重构过程中某些“看起来多余”的缓存被当成垃圾代码删掉了或者新结构里增加了多余的中间层调用。排查步骤用 profiler 工具比如 JProfiler、arthas 或者 Go 的 pprof对比重构前后热点函数的耗时分布。重点检查核心链路函数调用深度以及是否存在重复查询、重复计算。找到差异后优先恢复原有的性能优化措施比如缓存、预计算、短路判断。注意如果性能下降刚好出现在某个“历史提交里有明显优化痕迹”的代码上比如一个看起来“多余”的 static 变量缓存那大概率就是你动了不该动的优化逻辑。回退这一步保留结构改善即可。8.2 测试覆盖不到的老代码怎么处理真实业务里有些老代码就是没法覆盖——依赖对象创建复杂、依赖数据库数据、依赖外部接口。我的做法是先隔离给这些代码加接口包装让测试可以 mock 掉外部依赖。只写关键路径的集成测试如果单元测试实在写不出来那就写覆盖主链路的集成测试。保持锁定在确认覆盖之前不要对这个区域做结构变换。你只能先老老实实把保护网织好再谈重构。8.3 重构和功能迭代撞在一起如何排期重构最怕的是和功能迭代并行推进两者叠加会产生大量代码冲突和认知负担。我的经验是把它们严格分成两个不同的时间段如果功能迭代特别紧急那就优先做迭代重构推到迭代窗口之后。如果重构进行中突然插进来一个紧急需求我的做法是先在当前分支快速提交当前半成品状态可编译不破坏主干切回主干分支处理需求处理完之后再回到重构分支继续。只要保证重构分支和功能分支严格分离每次切换时都确保当前分支是可编译、可回退的状态就不会出大的问题。8.4 老板不认可重构的价值怎么办这个问题的解决方案其实是军规五的直接应用——选一个痛点最明显的模块小规模做一次样板重构。比如团队经常因为某个模块上线故障每两周就要熬夜救火。你可以用几天时间把那个模块的核心链路梳理清楚、补上测试、理顺结构然后把“故障率下降”“修复时长缩短”这些成果放在老板面前。数据永远比“我觉得这代码该重构了”更有说服力。样板建立起来之后后续再申请重构资源就会容易得多。我那次涨薪本质上就是老板从重构后的稳定运行里看到了实实在在的价值。重构不是目的降低维护成本、提高交付确定性才是。每次动手前先问自己这次重构保护网有没有拉好、变化点是不是足够小、有没有越界去改功能、代码是不是更易读、成果能不能被看见。这几条军规就是我贴在显示器上的东西。你也试试把公屏打在显示器上吧。