我刚工作第三年的时候接手过一个支付对账模块,那代码叫一个酸爽——三千多行挤在一个文件里,函数之间互相调用,变量名从 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.5 | 7.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 老板不认可重构的价值,怎么办
这个问题的解决方案,其实是军规五的直接应用——选一个痛点最明显的模块,小规模做一次样板重构。比如团队经常因为某个模块上线故障,每两周就要熬夜救火。你可以用几天时间把那个模块的核心链路梳理清楚、补上测试、理顺结构,然后把“故障率下降”“修复时长缩短”这些成果放在老板面前。
数据永远比“我觉得这代码该重构了”更有说服力。样板建立起来之后,后续再申请重构资源就会容易得多。我那次涨薪,本质上就是老板从重构后的稳定运行里看到了实实在在的价值。
重构不是目的,降低维护成本、提高交付确定性才是。每次动手前,先问自己:这次重构保护网有没有拉好、变化点是不是足够小、有没有越界去改功能、代码是不是更易读、成果能不能被看见。这几条军规,就是我贴在显示器上的东西。你也试试,把公屏打在显示器上吧。