ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

重读《重构》:小步重构与代码坏味道落地实践

重读《重构》:小步重构与代码坏味道落地实践 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. 关于系统重构的扩展思考这几年“系统重构”成了高频词很多团队把重构和“机房迁移”或“推倒重来”画上了等号。但读完这本书你会发现真正值得推荐的做法更像“模型重塑”而不是“系统重建”。我经历过一次大型系统重构那种动辄几十万行代码、几十个微服务的工程是不可能靠一部《重构》和几个技巧完成的。它的推进方式反而是把书中方法轮放大先把系统拆成领域模块再对每个模块做容量评估和数据建模分阶段替换、灰度发布、逐步下线旧代码。这个过程同时渗透了《重构》的“小步”和“不改变行为”原则——只不过“小步”放大到了发布的粒度而已。那类大规模系统改造有一个必须直面的事实代码逻辑可以在几周内重写但数据迁移、接口兼容、上下游依赖调整通常要花数月甚至更久。所以系统级重构的核心不在“写代码”而在“切流量”。你新旧两套并行逐步把流量从旧系统切到新系统每切一步都是“不改变可观察行为”的大号版本。这恰恰是《重构》方法论在架构层面的复用——用可控的小步骤把一次巨大的变更拆成几十个低风险的变更。如果你手头正有一个“机房重构”级别的项目在酝酿我的建议是先把推进方式定成“渐进式替换”再在每轮替换中都应用《重构》里“小步、验证、不改变行为”的原则。这样系统重构就不会变成“再造一个更大的轮子”。我个人在实际操作中的体会是重构是一种习惯不是一门技术。读这本书学到的最有价值的东西不是那几十个手法而是形成了一种条件反射——写代码时不停思考“这个函数是不是太长了”“这段逻辑是不是放错了地方”“如果改需求这次改动要影响多少个文件”。当这种反射深入骨髓的时候你维护代码的方式就彻底变了。如果你也在读这本书我建议你不要只画重点而是挑一个你自己维护的小模块照着书里示范一遍提炼、搬移、拆分你会有比读十遍都更深的体会。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进