1. 为什么我隔了十年又读了一遍重构
入行前几年,我一度觉得《重构:改善既有代码的设计》这书被神化了。提炼函数、以多态取代条件表达式,这些手法看起来平平无奇,不就是拆代码吗?直到我接手了一个跑了五年的老系统,那个模块叫订单中心,实际上是什么都往里塞的垃圾桶。计价逻辑散落在十几个文件里,优惠券、满减、会员折扣、运费规则互相纠缠,改一个需求要在不同方法之间跳来跳去。那时候我脑子里反复出现一句话:如果当初写代码的人读过重构,现在维护的人是不是就不用这么痛苦了。
后来我重读了第2版,才意识到这本书讲的从来不是“怎么把代码拆得漂亮”,而是“如何让代码在持续变化中活下去”。第2版最大的变化有两个:一是用 JavaScript 重写了所有示例,放下了“只适用于静态类型语言”的成见,二是加入了重构与性能、重构与架构的讨论,把重构从“术”的层面拉到了“道”的层面。网上那些关于“系统重构”的热搜词,很多时候说的是一次性的推倒重来,但我看书里讲的“重构”,其实是一系列持续进行的小改动——不改变代码可观察行为的前提下,改善内部结构。这个定义里的“不改变行为”四个字,就是重构与重写的分水岭。
这本书适合谁?适合已经能写出能跑的代码、却不知道怎么让代码变得好改的人。也适合那些正在酝酿“机房重构”级别的工程——比如把老系统推倒重来——但心里没底的人。读完之后你会发现,很多所谓的“必须重写”,其实是因为从未做过重构才积累到那个地步的。
2. 全书核心思想拆解:小步重构与代码坏味道
2.1 重构的定义与时机判断
作者给重构下的定义包含两层:名词层面,重构是对软件内部结构的一种调整,目的是在不改变可观察行为的前提下,提高其可理解性、降低修改成本;动词层面,重构是使用一系列重构手法来调整代码结构的过程。这个定义的关键在“不改变可观察行为”——不是“不改变行为”,而是“可观察行为”。换句话说,用户能感知到的输入输出不能变,但代码内部的调用关系、变量名、函数拆分、类结构,可以随便动。
这就把重构和“改bug”“加功能”彻底区分开了。加功能是改变行为,修bug是修正行为,重构是结构调整。很多新手犯的错,就是在加功能的时候顺手“重构”了一下,结果功能加错了,还分不清是逻辑错了还是重构破坏的。我自己的经验是:同一段时间只做一件类型的事。
时机判断是很多人的困惑点。书里提出一个非常务实的标准:三次法则——第三次理解同一段代码时,就该动手重构。第一次看是陌生,第二次看是似曾相识,第三次看还觉得这里绕,那就说明代码的表达方式有问题。我在实际项目里用过这个法则,发现它特别适用于接手的遗留代码。如果你发现自己在开会的时候需要频繁地在代码里“跳转”才能讲清楚某段逻辑,会议结束之后那段代码就值得重构。
2.2 代码坏味道的分类识别
第2版在第3章列出几十种代码坏味道,我挑几个实际项目里出现频率最高的展开说说。
第一是“神秘命名”。一个变量叫 temp,一个方法叫 doIt,一个类叫 Manager。这种坏味道最隐蔽,因为编译器不会报错,测试也不会挂,但它每天在消耗你的脑力。我接手过一个类叫 PayService,里面第一部分是拼订单数据,第二部分是调支付网关,第三部分是处理回调。这个名字本身就很“神秘”——不告诉你它到底服务谁。
第二是“过长函数”。书里有个观点我很认同:函数越长,越难看出它想做什么。不是单纯的“行数多”,而是分支多、嵌套深、局部变量多。一个函数里如果同时出现了订单构建、价格计算、库存扣减、通知发送,那这个函数就不只是长了,它是把四个不同的抽象级别揉在一起。
第三是“散弹式修改”:改一个需求要动四五个地方。这是最折磨人的坏味道,也是最需要重构来解决的。它的反面是“局部修改”——改一个需求只动一个地方。这两者的区别,基本就对应着低内聚和高内聚。
第四是“依恋情结”:某个函数对别的类的数据比对自家类更感兴趣。简单说就是代码放错了地方。我常看到订单相关逻辑里有个方法去读用户会员等级的字段,然后又去算用户折扣,这个逻辑就该住在用户那边。
第五是“重复代码”和“霰弹式修改”是近亲,但重复代码相对好识别。同一段逻辑出现在两个类里,或者同一个分支条件在不同的方法里反复出现,这两种情况都值得动手。
2.3 第2版与第1版的差异
第2版在目录结构上做了调整,把重构手法从目录式的罗列改成了按场景组织。比如“封装记录”“拆分阶段”“以查询取代派生变量”这些手法,在第一版里是没有的,它们脱胎于这些年函数式编程和领域建模的实践。
另一个显著变化是关于性能的讨论。第1版有一章专门讲重构与性能,强调“重构会降低性能”是个伪命题,正确的做法是先不管性能,最后再做优化。第2版延续了这个观点,补充了一句我很喜欢的话:大多数程序的大部分代码,对性能的影响都远小于想象。如果真想优化,应该先用性能剖析工具找到热点,而不是在重构的时候顺手做“自以为是的优化”。
3. 我认为最有价值的几项重构手法
3.1 提炼函数与以查询取代临时变量
“提炼函数”是全书最基础也最常用的手法,没有之一。核心理念是:函数名本身就是注释,把一段代码从一个大函数里拿出来,起一个能表达意图的名字,读代码的时候就省去了逐行解析的过程。我用一个例子来说明。
有一份代码是这样写的:
function calculateTotal(order) { let basePrice = order.quantity * order.itemPrice; let quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05; let shipping = Math.min(basePrice * 0.1, 100); return basePrice - quantityDiscount + shipping; }这不是最乱的状态,但已经可以提炼了。basePrice、quantityDiscount、shipping 这三件事是三个独立的语义单元,挤在一个函数里。按照“以查询取代临时变量”的思路,可以把 basePrice 提成一个函数:
function basePrice(order) { return order.quantity * order.itemPrice; } function quantityDiscount(order) { return Math.max(0, order.quantity - 500) * order.itemPrice * 0.05; } function shippingCost(order) { return Math.min(basePrice(order) * 0.1, 100); }提炼之后,每个函数都可以单独测试,命名表达了意图,调用方一眼就能看出总价由哪些部分组成。更重要的是,当促销规则调整时,你只需要改对应那个小函数,不用在大函数里满屏找。
3.2 拆分阶段与以多态取代条件表达式
“拆分阶段”是我在重构老系统时最常用、也是最有效的手法。它的核心思路是把一段复杂的流程拆成多个独立的阶段,每个阶段只做一件事,阶段的输入和输出都清晰定义。最典型的场景是订单处理:校验 → 价格计算 → 库存锁定 → 支付 → 发送通知。在遗留项目里,这五个阶段经常被糅在一起,穿插出现,互相污染。
拆分成阶段之后,每个阶段可以独立修改和测试,不同阶段的错误不会互相影响。而且阶段之间的数据流变得清晰——你看到的每个函数三五行,参数明确,返回值明确,不用心里维护一个“到了这一步 session 里的 user 已经被修改,库存已经扣减”的隐式状态。
“以多态取代条件表达式”这个手法解决的是另一类问题:if-else 或 switch 分支随着业务发展不断膨胀。比如运费计算,一开始有普通快递和顺丰两种,后来加了冷链、同城急送、货到付款,每种都有不同的算价规则。继续在同一个函数里堆条件,函数迟早会爆炸。
给每个运输方式建一个实现同一接口的类,调用方只需要一句shippingStrategy.calculate(order),新增一种运输方式就是新增一个类,不改任何现有逻辑。这就是开闭原则——对扩展开放,对修改关闭——在重构层面的落地。
3.3 搬移函数与隐藏委托关系
“搬移函数”解决的是代码放错地方的问题。书里的判断方法很实用:一个函数频繁被其他模块引用,和另一个模块交流更多,它就该住到那个模块去。判断标准不是当前代码怎么写的,而是函数真正依赖的数据在哪里、被谁频繁调用。
“隐藏委托关系”是把中间人模式用在了正确的方向:当客户对象需要调用服务对象的某个能力时,不让它直接拿到服务对象再往里钻,而是在客户对象上封装一个方法,把委托关系藏起来。这样做的好处是修改委托链时,只有封装处需要改动,客户端代码纹丝不动。
4. 如何把方法论落地到真实项目
4.1 重构前的测试准备
书里对测试的作用强调到了近乎偏执的程度:重构前必须有测试,没有测试就必须先补测试再动手。这不是教条,而是我在实际项目里踩过坑之后才真正认同的规律。
测试提供的是一张安全网。发展:重构的每一步都要求“不改变可观察行为”,可你怎么知道行为没变?如果你的代码没有任何自动化校验,唯一的“可观察行为”就是用户点出来的结果,那每一步改动都是在裸奔。我见过太多“重构到一半线上出问题,回滚又找不到版本”的惨剧,根因就是没有测试兜底。
对于遗留代码,补测试的策略是:不要试图一次性把覆盖率补到80%以上。优先给“准备重构的那段代码”加上特征测试,把当前的输入输出样例固化下来。这里的建议是:先把稳定接口的当前行为用测试锁住,再开始重构。你不用保证测试覆盖所有逻辑分支,只要覆盖率覆盖了主流程和几个边界,重构时能第一时间发现行为被改变就够了。
4.2 小步重构与持续集成
小步重构是整个方法论的执行灵魂。“小步”不是指改动小,而指每一步都立即可验证。每一步之后,编译能过、测试能过,再走下一步。
怎么做到小步?答案是频繁提交。不要攒了一个下午的大改动才 commit 一次,而是每次做完一个原子的、可验证的改动就提交。重构阶段即使提交信息写的是“WIP”也没关系,关键是每一步都处于可运行状态,出了任何问题,用git log和git diff就能精确定位到是哪一改动引入的。
把持续集成纳入重构流程后,保护更强了。每次提交触发构建和测试,只要哪里破坏,构建直接标红,问题在几分钟内暴露,不会积压到发布当天才爆发。我们把“重构 + 写完就提交 + CI全绿”绑定成一条铁律之后,重构的心里负担降了一个量级。
4.3 经典案例:一次订单模块重构实录
我挑一个真实经历来展示完整流程。系统里有个函数叫generateOrder,同时负责商品校验、价格计算、折扣计算、生成订单实体、更新库存、发送短信。一共 300 多行,中间嵌套了四层 if-else,零星夹杂着 console.log 调试输出。
第一步,我先给这个函数补了特征测试:构造几种典型输入——正常下单、库存不足、折扣码过期、商品已下架——把当前的输出结果完整记录下来,写成断言。
第二步,提炼函数。把商品校验、价格计算、库存更新、短信发送各拆成独立函数。这一步纯粹是机械运动,不做任何逻辑修改。每次拆完一个函数,就跑一遍测试,确保绿。
第三步,用“拆分阶段”重构主流程。我把generateOrder改成了清晰的五段式:校验阶段 → 计价阶段 → 库存阶段 → 通知阶段。每个阶段的输入参数和返回值都明确,不再通过函数内的局部变量暗相传递。
第四步,处理条件逻辑。计价阶段里原来有一大段优惠金额计算,是三个 if-else 嵌套。我用“以多态取代条件表达式”重写成了四种优惠策略类,每个类只负责一种促销类型的计算。
最终结果:原函数从 300 多行降到 40 行,每一阶段的函数名都精确表达了职责。测试从 0 个增加到 15 个,覆盖率从 20% 提到了 78%。最关键的是,这次重构分成了大约 20 次小步骤提交,每次提交 CI 都是绿色。
5. 重构过程中的常见问题与避坑清单
5.1 什么时候不该动手重构
书里明确表示,有一种情况不要做重构:系统已经烂到无法安全运行,而重构的每一步都建立在现有行为之上,可现有行为本身就是坏的。这种情况下,你更需要的是重写或者逐步替换,而不是重构。
实际项目里还有一种情况:代码马上要被新的实现取代。比如知道下季度这个模块要接第三方的新接口,整体方案都要换,那现在就没有必要去优化内部的临时逻辑。
还有一类极易忽略的情况:在 deadline 前重构。重构虽然不改变行为,但它会改变代码结构、影响团队成员对代码的热悉度。项目发布前 3 天,任何结构上的改动都是一种风险引入,哪怕测试全绿,也难以保证没有漏网之鱼。
我个人的经验法则:重构一定要独立于“功能开发”和“发布计划”单独排期,或者至少是功能开发前的准备步骤,而不是功能开发中的附带操作。
5.2 与团队协作有关的阻力
重构最大的敌人不是代码,是团队里“能不能别动我代码”的抵触情绪。解决这个问题不是靠说服,而是靠建立信任机制——测试、CI、代码评审。当团队逐步认可“重构不会改坏功能”时,阻力自然变小。
协作层面的另一个关键点是:重构要小批量地做,不要独立部署重构。如果一次重构产生了几十个文件的 diff,评审者根本看不完,也不会有能力判断每个改动是否正确。把重构分成多次提交,每次提交尽量只影响一小片区域,代码评审的负担就小,合并的阻力也就小。
这里还有一个实用技巧:重构期间保持和产品经理的沟通。重构对用户来说是无感知的,但如果你重构到一半要把某些逻辑拆开,可能会导致后端接口的返回结构变化,这就介于重构和接口升级之间了。这种情况必须提前告知下游调用方,避免“静默重构”把合作方炸了。
5.3 测试缺失的代码怎么处理
真实世界很残酷:很多待重构的模块根本没有测试。我的处理顺序是:
先从入口函数开始,记录当前的主要输入和输出组合,写成单元测试或简单的集成测试。数量不必多,把主流程和几个让它报错过的边界场景锁住即可。如果模块没有现成的测试框架,那就先搭一个最轻量的测试脚手架,哪怕只是临时脚本,在重构过程中手动触发也行。
重构完成后,再基于重构后的接口完善测试。重构前的测试是锁定行为用的,重构后的测试才是保护未来用的。两者目的不同,工具可以一样,但心智上要区分清楚。
5.4 常见问题速查表
| 问题 | 可能原因 | 处理建议 |
|---|---|---|
| 重构后测试失败 | 重构意外改变了行为 | 立即回退到上一个绿版本,通过 diff 定位具体改动 |
| 重构后无测试覆盖 | 旧代码缺失测试 | 先用特征测试锁定行为,再动手 |
| 重构范围越滚越大 | 未按“小步”执行 | 拆成多个原子步骤,每次只处理一个坏味道或手法 |
| 团队成员抵触 | 缺乏信任机制 | 建立 CI 和测试保护,小批量提交并配合评审 |
| 重构后性能下降 | 产生了不必要的间接层 | 用性能剖析工具定位热点,只修热点,别盲目回退 |
| 重构到一半被叫停 | 排期冲突或需求变更 | 保留已完成的步骤提交,未完成的步骤另开分支等待时机 |
6. 关于系统重构的扩展思考
这几年“系统重构”成了高频词,很多团队把重构和“机房迁移”或“推倒重来”画上了等号。但读完这本书你会发现,真正值得推荐的做法更像“模型重塑”,而不是“系统重建”。
我经历过一次大型系统重构,那种动辄几十万行代码、几十个微服务的工程,是不可能靠一部《重构》和几个技巧完成的。它的推进方式反而是把书中方法轮放大:先把系统拆成领域模块,再对每个模块做容量评估和数据建模,分阶段替换、灰度发布、逐步下线旧代码。这个过程同时渗透了《重构》的“小步”和“不改变行为”原则——只不过“小步”放大到了发布的粒度而已。
那类大规模系统改造有一个必须直面的事实:代码逻辑可以在几周内重写,但数据迁移、接口兼容、上下游依赖调整,通常要花数月甚至更久。所以系统级重构的核心不在“写代码”,而在“切流量”。你新旧两套并行,逐步把流量从旧系统切到新系统,每切一步都是“不改变可观察行为”的大号版本。这恰恰是《重构》方法论在架构层面的复用——用可控的小步骤,把一次巨大的变更拆成几十个低风险的变更。
如果你手头正有一个“机房重构”级别的项目在酝酿,我的建议是:先把推进方式定成“渐进式替换”,再在每轮替换中都应用《重构》里“小步、验证、不改变行为”的原则。这样,系统重构就不会变成“再造一个更大的轮子”。
我个人在实际操作中的体会是:重构是一种习惯,不是一门技术。读这本书学到的最有价值的东西,不是那几十个手法,而是形成了一种条件反射——写代码时不停思考“这个函数是不是太长了”“这段逻辑是不是放错了地方”“如果改需求,这次改动要影响多少个文件”。当这种反射深入骨髓的时候,你维护代码的方式就彻底变了。如果你也在读这本书,我建议你不要只画重点,而是挑一个你自己维护的小模块,照着书里示范一遍提炼、搬移、拆分,你会有比读十遍都更深的体会。