每日好书推荐:《重构:改善既有代码的设计》

每日好书推荐:《重构:改善既有代码的设计》

推荐日期:2026-05-04

作者:Martin Fowler(马丁·福勒),第2版示例以 JavaScript 为主

英文名:Refactoring: Improving the Design of Existing Code

适合谁读:写过一段时间代码、开始维护老项目、经常被“祖传代码”折磨的开发者;技术负责人、架构师、测试工程师也很适合。

一句话推荐:这本书教你把“能跑但难改”的代码,一小步一小步变成“能跑、好懂、好改”的代码。

先说结论:这本书到底解决什么问题?

很多人以为写代码的难点是“从0到1写出来”。但真实项目里,更大的难点往往是:代码已经存在,功能也能跑,可是没人敢改。变量名看不懂,函数又长又绕,业务规则散落在各处,一改就炸。这个时候,不是推倒重来,而是要重构

重构的核心定义很朴素:在不改变软件外部行为的前提下,改善代码内部结构。换成大白话就是:用户看到的功能不变,但程序员看到的代码变清楚、变稳、变容易维护。

马丁·福勒这本书的主线可以概括为四句话:

  1. 先保证安全:重构不是凭感觉大改,必须有测试兜底,小步修改、频繁验证。
  2. 先闻到坏味道:重复代码、超长函数、奇怪命名、到处传参数、复杂条件,都是代码在向你求救。
  3. 再选合适手法:提炼函数、封装变量、搬移函数、拆分阶段、以多态取代条件等,都是有章法的工具。
  4. 长期持续改进:重构不是“有空做一次大扫除”,而是平时开发时随手整理,让代码始终保持可演进。

第1章:重构,第一个示例——先看一场“代码整理现场”

第一章不是先讲大道理,而是直接拿一个计费程序做示范。原始代码能跑,但逻辑混在一起:既算金额,又算积分,还负责拼输出文本。它的问题不在于“错”,而在于“难读、难扩展”。

作者的第一步是建立测试,确认现在的输出是什么。因为重构的底线是外部行为不变,测试就像安全绳。没有安全绳,你以为自己在整理代码,其实可能在制造事故。

接着,作者把一个大函数逐步拆开:把金额计算提炼成函数,把积分计算提炼成函数,把临时变量替换成查询函数。这样做的结果是:每个小函数只回答一个问题,名字也能直接表达意图。

然后,作者把“计算”和“格式化输出”分成两个阶段。大白话说,就是别让一个人同时做会计、排版、客服。先把账算清楚,再决定怎么展示。这样以后如果要输出 HTML、JSON 或纯文本,就不用改核心计算逻辑。

最后,作者用多态把不同戏剧类型的计费规则分开。原来靠 if/switch 判断类型,现在让不同类型各自负责自己的算法。重点不是“炫技用面向对象”,而是把变化点放到合适的位置:以后新增类型,就少改旧代码。

这一章想告诉读者:重构不是一次性大手术,而是一串非常小的动作。每一步都很小,小到可以测试;但积累起来,代码结构会发生质变。

第2章:重构的原则——什么时候该改,什么时候别乱改

这一章给重构立规矩。首先,重构有一个严格边界:不添加新功能,不修复可见行为,只改善内部结构。如果你一边加功能一边整理代码,就要知道自己在换“帽子”:开发新功能是一顶帽子,重构是一顶帽子。两顶帽子可以交替戴,但不要同时戴,否则出了问题很难定位。

为什么要重构?第一,重构能改善设计。代码如果长期只加不整理,设计会慢慢腐烂。第二,重构能让代码更容易理解。机器不在乎代码好不好懂,人会在乎。第三,重构能帮助发现 bug。因为你把逻辑拆清楚以后,隐藏的矛盾更容易暴露。第四,重构能让开发更快。短期看像是在“停下来整理”,长期看是在减少每次改动的阻力。

什么时候重构?作者强调“见缝插针”。加功能前,如果旧结构挡路,先整理一下;修 bug 时,如果发现代码看不懂,先把逻辑理清;做代码评审时,如果发现坏味道,也可以顺手重构。最好的重构不是开一个“重构大项目”,而是日常开发中的持续清洁。

重构也有挑战。数据库、公开 API、复杂继承、性能敏感代码、缺少测试,都会提高风险。作者不是鼓励盲目重构,而是提醒:越危险的地方,越要小步、测试、沟通。

关于架构,作者的态度很实用:不要幻想一开始就设计完美。可以先做够当前需求的设计,再通过重构逐步演进,这和 YAGNI(你不会需要它)是一致的。架构不是神谕,而是可以在反馈中成长。

关于性能,作者也很清醒:不要为了想象中的性能牺牲清晰度。大多数时候,先写清楚,再测量瓶颈,再针对性优化。清楚的代码反而更容易优化。

第3章:代码的坏味道——代码不会说话,但会“发臭”

这一章由 Kent Beck 和 Martin Fowler 总结了常见坏味道。坏味道不是绝对错误,而是提示你:“这里可能值得看看”。

  • 神秘命名:变量、函数、类的名字看不出意图。解决方向是改名,让名字讲人话。
  • 重复代码:同样逻辑出现多次。重复越多,修 bug 越容易漏。常用提炼函数、搬移函数来消除。
  • 过长函数:一个函数塞太多事。它通常需要被拆成一组有名字的小函数。
  • 过长参数列表:函数需要一长串参数,说明数据可能应该打包成对象,或者函数依赖关系不清。
  • 全局数据:谁都能改,谁都可能背锅。应该封装访问入口,减少失控修改。
  • 可变数据:数据被多处修改,状态变化难追踪。可以用封装、拆分变量、减少共享可变状态来控制。
  • 发散式变化:一个模块因为多种原因被频繁修改。说明职责混杂,需要拆分。
  • 霰弹式修改:一个小需求要改很多地方。说明相关逻辑散落,需要聚合到一起。
  • 依恋情结:一个函数总是访问别的对象的数据,说明它可能应该搬到那个对象附近。
  • 数据泥团:几个字段总是一起出现,比如 start/end、x/y。它们可能应该组成一个对象。
  • 基本类型偏执:什么都用字符串、数字表示,业务含义藏不住。可以用小对象表达领域概念。
  • 重复的 switch:同样的类型判断散落各处。可以考虑多态或集中管理变化点。
  • 循环语句:循环本身不是坏事,但复杂循环会藏逻辑。可以拆分循环或用管道表达。
  • 冗赘的元素:类、函数、接口存在但没提供价值。该删就删,别为形式而形式。
  • 夸夸其谈通用性:为了未来可能的需求搞一堆抽象。未来没来,复杂度先来了。
  • 临时字段:对象里有些字段只在特殊场景使用,平时为空。说明对象职责可能不稳定。
  • 过长的消息链:调用像 a.getB().getC().getD() 一路挖。调用者知道太多内部结构。
  • 中间人:一个类只是转发请求,不做实事。该移除就移除。
  • 内幕交易:模块之间过度访问彼此内部。边界不清,容易牵一发动全身。
  • 过大的类:一个类知道太多、做太多。需要提炼类或拆职责。
  • 异曲同工的类:两个类做类似事情却接口不同。可以统一接口或合并抽象。
  • 纯数据类:只有字段和 getter/setter,没有行为。可能需要把相关行为搬进去。
  • 被拒绝的遗赠:子类继承了一堆不想要的父类能力。继承关系可能错了。
  • 注释:注释本身不是坏事,但如果注释是在解释糟糕代码,优先让代码自解释。

这一章最有价值的地方是:它给程序员一套“诊断词典”。你不用凭模糊感觉说“这段代码怪怪的”,而可以说:“这里是过长函数加数据泥团,建议提炼函数并引入参数对象。”

第4章:构筑测试体系——没有测试,重构就是裸奔

重构最怕的是:你以为没改行为,结果行为悄悄变了。所以作者强调自测试代码。所谓自测试,就是程序自己能告诉你“我现在还对不对”。

这一章用示例说明怎样从第一个测试开始。不要等测试体系完美了再写测试,先给关键行为加一个能跑的测试。然后逐步补充更多场景,尤其是边界条件。边界条件往往是 bug 最爱躲的地方,比如空值、零、最大值、临界日期、异常输入。

作者还强调测试夹具,也就是测试前准备好的对象和数据。好的测试夹具能让测试更清楚,不用每个测试都重复搭环境。

这章的大白话结论是:测试不是为了证明你很专业,而是为了让你敢改。没有测试时,人会越来越怕碰老代码;有测试时,重构才有底气。

第5章:介绍重构名录——每个手法都要讲清楚“何时用、怎么做”

从第6章开始,书进入大量具体重构手法。第5章相当于使用说明书,告诉你每个重构条目会怎样展开:名称、动机、做法、示例。

这很重要,因为重构不是“灵感创作”,而是工程动作。一个好重构手法应该说清楚:它解决什么坏味道,适用什么场景,步骤如何拆小,风险在哪里,怎样验证。

作者也提醒,选择重构要看上下文。没有哪一种手法永远正确。比如提炼函数能提升可读性,但如果函数名起不好,反而更绕;封装能保护数据,但过度封装也会让代码啰嗦。重构是判断力和工具箱的结合。

第6章:第一组重构——最常用的基础动作

这一章是重构工具箱的入门套装,几乎每天都能用。

  • 提炼函数:把一段有独立含义的代码抽成函数,并用好名字表达意图。它能降低阅读负担。
  • 内联函数:如果函数内容和名字一样简单,或者抽出来反而绕,就把它放回去。重构不是只会拆,也要会合。
  • 提炼变量:复杂表达式看不懂时,用一个变量名解释中间含义。
  • 内联变量:如果变量只是重复表达式,没有增加理解价值,就去掉。
  • 改变函数声明:函数名、参数、返回值不合适时就调整,因为函数声明是模块之间的契约。
  • 封装变量:别让重要数据到处被直接访问,先包一层访问函数,给未来控制变化留入口。
  • 变量改名:命名是最便宜也最有效的重构之一。好名字能省掉很多注释。
  • 引入参数对象:一组参数总是一起出现,就把它们变成一个对象,让关系显性化。
  • 函数组合成类:一堆函数围绕同一组数据工作,可以把数据和函数放进一个类里。
  • 函数组合成变换:如果一组函数都是把原始数据加工成派生数据,可以集中成一个变换过程。
  • 拆分阶段:一段代码同时做两类事情,就拆成两个阶段,比如先解析、再执行;先计算、再展示。

这一章的精神是:先把意图显露出来。很多烂代码并不是算法多难,而是“意图被实现细节淹没了”。

第7章:封装——把该藏的藏起来,把该表达的表达出来

封装不是为了把代码包得神神秘秘,而是为了控制变化影响范围。

  • 封装记录:如果数据结构只是裸字段,外部到处依赖字段名,未来改字段会很痛。封装后可以通过访问函数保护变化。
  • 封装集合:集合不能随便暴露给外部直接改,否则对象不变量会被破坏。应该提供受控的添加、删除方式。
  • 以对象取代基本类型:比如电话号码、金额、日期范围,不只是字符串或数字,它们有规则和行为,适合变成对象。
  • 以查询取代临时变量:临时变量如果表达的是某种可计算含义,可以变成查询函数,让逻辑更可复用。
  • 提炼类:一个类做太多事,就把其中一组相关字段和行为抽出去。
  • 内联类:如果一个类太瘦、没有独立价值,就合回去,减少不必要结构。
  • 隐藏委托关系:调用者不应该知道太多中间对象,让被调用对象提供更直接的方法。
  • 移除中间人:如果一个对象只会转发,没有隐藏复杂度,就别让它挡路。
  • 替换算法:当你找到更清楚、更可靠的算法时,可以整体替换,但前提是测试覆盖行为。

这一章背后的判断标准是:让外部依赖稳定的概念,而不是内部细节。代码越少暴露细节,未来越好改。

第8章:搬移特性——东西应该放在最懂它的地方

很多代码变乱,是因为函数、字段、语句放错了位置。第8章专门讲“搬家”。

  • 搬移函数:函数总是使用某个模块的数据,就考虑搬到那个模块。让行为靠近数据。
  • 搬移字段:字段如果更属于另一个对象,就搬过去。数据位置决定了依赖方向。
  • 搬移语句到函数:某些语句每次调用函数前后都要写,就把它们并入函数,减少重复仪式。
  • 搬移语句到调用者:如果函数内部某些语句只适合部分调用场景,就移出去,让函数更纯粹。
  • 以函数调用取代内联代码:同样逻辑已经有函数了,就别复制粘贴,直接调用。
  • 移动语句:把相关语句靠近放置,让阅读顺序更符合数据流。
  • 拆分循环:一个循环里做多件事,拆开后每个循环表达一个目的,之后更容易优化或复用。
  • 以管道取代循环:对于过滤、映射、聚合类逻辑,用管道式表达能更直接说明“数据怎么流动”。
  • 移除死代码:不用的代码不要留着当纪念品。版本控制会记住历史,代码库应该保持清爽。

这章的核心问题是:读者看到一段逻辑时,能不能自然找到它?如果总要跨文件、跨类追来追去,就说明位置可能不对。

第9章:重新组织数据——数据结构一乱,逻辑迟早跟着乱

程序的很多复杂度来自数据。数据表达不清,函数再漂亮也救不了。

  • 拆分变量:一个变量被拿来表示多个含义,就拆成多个变量。变量应该“一生只干一件事”。
  • 字段改名:字段名会传播到很多地方,名字不准会长期误导人。
  • 以查询取代派生变量:如果某个值可以从其他数据算出来,且保持同步很麻烦,就用查询函数动态计算。
  • 将引用对象改为值对象:如果对象只由值决定身份,比如金额、坐标、日期范围,可以用值对象,减少共享状态问题。
  • 将值对象改为引用对象:如果对象有独立身份,需要被多处共享并保持一致,比如客户、账户,就用引用对象。

这一章最重要的是区分“值”和“身份”。两个 100 元金额通常可以视为相等的值;但两个用户即使名字一样,也不是同一个身份。这个区别会影响对象建模。

第10章:简化条件逻辑——别让 if/else 长成迷宫

条件逻辑是业务代码最常见的复杂度来源。第10章教你把条件写得更像人话。

  • 分解条件表达式:复杂 if 条件、then 分支、else 分支都可以提炼成函数,用名字解释含义。
  • 合并条件表达式:多个条件导致同一结果,就合并成一个表达式,减少重复分支。
  • 以卫语句取代嵌套条件表达式:先处理特殊情况并提前返回,主流程就不用被层层嵌套包住。
  • 以多态取代条件表达式:当条件是根据类型选择行为时,可以让不同类型自己实现行为。
  • 引入特例:对于空对象、未知客户、默认用户这类特殊情况,可以建一个特例对象,避免到处判空。
  • 引入断言:把代码运行所依赖的前提明确写出来,帮助读者和程序尽早发现不合理状态。

这章的大白话原则是:正常路径要清楚,特殊路径要明确。不要让读者在一层又一层缩进里找主线。

第11章:重构 API——接口设计就是沟通方式

API 是代码之间的边界。边界设计得好,调用者轻松;边界设计得差,所有人都痛苦。

  • 将查询函数和修改函数分离:一个函数要么回答问题,要么改变状态,最好别两者混在一起,避免调用者被副作用坑到。
  • 函数参数化:多个函数逻辑类似、只是常量不同,可以合成一个带参数的函数。
  • 移除标记参数:函数传 true/false 来决定两套行为,调用处很难懂,通常应拆成两个明确函数。
  • 保持对象完整:如果函数需要对象的多个字段,直接传整个对象,而不是拆成一堆零件。
  • 以查询取代参数:如果参数可以由被调用方自己算出来,就别让调用者传,减少出错机会。
  • 以参数取代查询:反过来,如果函数不该依赖某个外部查询,就把结果作为参数传入,让依赖更明确。
  • 移除设值函数:不该被随意修改的字段,就不要提供 setter。对象创建后保持稳定更安全。
  • 以工厂函数取代构造函数:当创建逻辑有多种意图或需要隐藏具体类型时,工厂函数更清楚。
  • 以命令取代函数:当一个操作很复杂,需要状态、撤销、排队、日志时,可以变成命令对象。
  • 以函数取代命令:如果命令对象已经过度复杂,而实际只是简单操作,就退回普通函数。

这一章的关键是权衡:参数少不一定好,函数多不一定坏。好 API 应该让调用者一眼知道“我要做什么”,同时减少误用空间。

第12章:处理继承关系——继承能帮忙,也能添乱

继承是强工具,但也容易造成强耦合。最后一章讲如何整理继承体系。

  • 函数上移:多个子类有相同方法,就提到父类,消除重复。
  • 字段上移:多个子类有相同字段,也可以上移到父类。
  • 构造函数本体上移:子类构造过程有共同部分,就提到父类构造逻辑中。
  • 函数下移:父类方法只对部分子类有意义,就下移到真正需要的子类。
  • 字段下移:父类字段并非所有子类都需要,也应该下移。
  • 以子类取代类型码:对象里有 type 字段决定行为时,可以用子类表达不同类型。
  • 移除子类:如果子类差异很小或不再需要,就合并回去,别保留空架子。
  • 提炼超类:多个类有相似行为时,可以抽出共同父类。
  • 折叠继承体系:父类和子类区别不大,就合并,减少层级。
  • 以委托取代子类:当子类只是表达一种可变角色或策略时,用组合/委托更灵活。
  • 以委托取代超类:如果继承只是为了复用一部分能力,却带来不合适的父子关系,就改用委托。

这一章的底层思想是:不要迷信继承。继承适合表达稳定的“是一种”关系;如果只是想复用代码,或者对象角色会变化,组合和委托常常更稳。

读完这本书,最该带走的实践清单

  1. 改代码前先找安全绳:没有测试就先补关键测试,至少覆盖你准备动的行为。
  2. 小步前进:一次只做一个小重构,跑测试,再继续。别把十个想法揉成一次提交。
  3. 命名优先:如果你不知道怎么重构,先把变量、函数、类的名字改准,很多结构问题会自己浮出来。
  4. 消除重复:重复代码不是省事,是未来维护成本的贷款。
  5. 让行为靠近数据:函数总是用谁的数据,就考虑搬到谁身边。
  6. 把复杂条件变成清楚结构:提炼条件、卫语句、特例对象、多态,都是为了让主线更明白。
  7. 公开 API 要慎重:内部代码好改,公开接口难改。改 API 时要考虑调用者迁移成本。
  8. 别过度设计:为真实变化重构,不要为脑补的未来堆抽象。
  9. 持续整理:重构像刷牙,靠日常习惯,不靠一年一次大扫除。

为什么今天推荐它?

《重构》是少数能真正改变程序员工作方式的书。它不是讲宏大架构,也不是堆概念,而是把“如何把一段糟糕代码慢慢变好”拆成了可操作动作。

对于小白来说,这本书能帮你建立代码审美:什么叫清楚,什么叫职责单一,什么叫变化点,什么叫坏味道。对于有经验的开发者来说,它能提醒你:真正厉害的工程能力,不只是写新代码快,而是让旧代码也能继续生长。

如果你的项目里有“不敢碰”的模块,这本书就是一套拆弹手册。它不会让坏代码一夜变完美,但会教你每次安全地变好一点。