一、全书概览
这本书是软件开发领域的经典之作。1999 年第 1 版出版时,"重构"这个词对大多数程序员来说还很陌生。Martin Fowler 用一本书的篇幅,把"在不改变软件可观察行为的前提下,改善其内部结构"这件事,从一门手艺变成了一套有章可循的工程实践。
第 2 版在 2018 年发布,距第 1 版整整 19 年。这 19 年里,行业发生了巨大变化:JavaScript 从"玩具语言"变成了无处不在的主力语言,函数式编程从学术圈走向了生产环境,测试驱动开发(TDD)从边缘实践变成了主流方法论。Fowler 做了一个大胆但正确的决定——把书中的代码示例从 Java 换成了 JavaScript,同时增加了函数式编程的重构示例。
全书结构清晰:前 5 章讲"为什么重构、什么时候重构、怎么安全地重构",后面 12 章是具体的重构手法目录(catalog),每条手法都有名字、动机、做法和示例。第 1 版收录了 70 多个重构手法,第 2 版精简到了 60 多个,删掉了一些在实践中证明价值不大的条目,也补充了一些新的。
这本书的定位不是"读完就能写好代码",而是给你一把尺子——让你知道代码什么时候开始变坏,以及怎么把它改好。它解决的是一个真实存在的痛点:大部分程序员花在阅读和修改现有代码上的时间,远超写新代码的时间。
核心数据
| 维度 | 内容 | |------|------| | 初版 | 1999 年,Java 示例,72 个重构手法 | | 二版 | 2018 年,JavaScript 示例,精简至约 60 个重构手法 | | 新增内容 | 函数式编程重构示例、测试体系章节大幅重写 | | 行业影响 | 直接催生了 Refactoring 浏览器插件、IDE 重构菜单等工具 | | 适用读者 | 有 1 年以上实际编码经验的开发者 |
二、逐章要点
第 1 章:重构,第一个示例
这一章用一个非常具体的例子开场:一个戏剧演出计费系统。代码能跑,但逻辑混乱——一个 statement() 函数里塞了计费逻辑、格式化输出、数据计算等所有东西,超过 10 行嵌套的 switch-case。
Fowler 手把手演示了如何把这个"能工作但让人头疼"的代码,一步步重构成结构清晰、易于理解和修改的形式。整个过程的关键步骤包括:
- 先写测试,确保重构不破坏已有功能
- 用 Extract Function 把大函数拆小
- 用 Move Function 把逻辑挪到更合适的位置
- 用 Replace Temp with Query 消除临时变量
- 用 Replace Conditional with Polymorphism 用多态替代条件判断
原文摘录: "重构的关键在于,每次只做一小步修改,做完就跑测试。如果测试挂了,就能立刻定位到问题出在刚才那一步。"
这个示例之所以有效,是因为它不是玩具代码——它有真实的业务复杂度,而且改动过程完全是增量式的。读者能清楚地看到:代码不是一次性重写的,而是通过几十个小步骤逐渐变好的。
第 2 章:重构的原则
这一章回答了三个根本问题:什么是重构、为什么要重构、什么时候重构。
定义:重构是对软件内部结构的一种改变,目的是在不改变软件可观察行为的前提下,使其更容易理解、更容易修改。
"两顶帽子"比喻是 Fowler 最经典的类比之一:你要么在"添加功能"(添加新行为),要么在"重构"(不改变行为)。不要同时戴两顶帽子。这个区分看似简单,实际上是很多团队重构失败的根源——一边改结构一边加功能,出了 bug 都不知道是哪个导致的。
| 重构原则 | 具体做法 | |----------|----------| | 小步前进 | 每次只做一个小的、可验证的修改 | | 持续测试 | 每改一步就跑测试,测试不过就回退 | | 何时重构 | 添加功能时、修 bug 时、代码审查时 | | 何时不重构 | 代码太烂不值得救、即将重写、deadline 压迫 |
- [ ] 下次改 bug 时,先问自己"这段代码能不能趁这个机会改得更好一点"
- [ ] 在团队 code review 中加入"代码坏味道"检查项
第 3 章:代码的坏味道
这是全书最实用的一章,也是被引用最多的一章。Fowler 列出了 20 多种"坏味道"(Code Smells),每种都配有识别方法和对应的重构手法。
所谓"坏味道",就是那些让你隐约觉得代码有问题、但又说不清到底哪里不对的信号。就像走进一个房间,虽然没看到灰尘,但能闻到空气不太对。
核心的坏味道包括:
结构型坏味道:
- Duplicated Code(重复代码):最常见的坏味道。如果同一段逻辑出现在两个以上地方,那就是重构的信号。
- Long Function(过长函数):Fowler 引用了 Kent Beck 的话:"每当感觉需要写注释来说明某段代码在做什么的时候,就把这段代码提取成一个函数,函数名就是那个注释。"
- Large Class(过大类):一个类如果超过 200 行,大概率在做太多事情。
- Long Parameter List(过长参数列表):超过 3 个参数就该考虑封装成对象了。
演进型坏味道:
- Divergent Change(发散式变化):一个类因为不同的原因被反复修改——说明这个类的职责不够单一。
- Shotgun Surgery(散弹式修改):一个改动需要同时修改多个类——说明逻辑被分散在了错误的地方。
- Feature Envy(依恋情结):一个函数大量使用另一个类的数据,却不在那个类里——它应该搬过去。
我的看法:第 2 版中,Fowler 对"坏味道"的讨论更加务实了。他承认有些"味道"在现代框架下不再那么刺鼻——比如有些框架本身就鼓励一定的重复(Convention over Configuration)。这不是否定重构的价值,而是提醒读者不要教条化。
- [ ] 打开自己最近在维护的项目,数一下"Long Function"和"Duplicated Code"出现了多少次
- [ ] 把"坏味道清单"打印出来贴在工位上
第 4 章:构筑测试体系
第 2 版对这一章做了大幅重写,因为 2018 年的测试工具和理念比 1999 年先进太多了。Fowler 用 JavaScript 的测试框架(主要是 Jest 风格的 describe/it 结构)来演示。
核心观点没变:没有测试的保护,重构就是在走钢丝。
但 Fowler 的新补充很有价值:
- 不完美的测试也比没有测试好。不要追求 100% 覆盖率才开始重构。哪怕只有几个关键的冒烟测试,也比裸奔强。
- 测试的目的不是证明代码正确,而是给你改代码的信心。这是一个重要的认知转变。
- 重构过程中的测试和 TDD 中的测试角色不同。TDD 的测试是设计工具,重构的测试是安全网。
原文摘录: "我经常看到人们把测试写得过于脆弱——一个微小的实现改动就导致大量测试失败。这样的测试不是安全网,而是枷锁。"
这一章还讨论了一个很实际的问题:面对遗留代码(完全没有测试的老代码),怎么开始?Fowler 的建议是" characterization tests"(特征测试)——先为现有行为编写测试,不管这些行为是否"正确",先把当前行为固定下来,然后才能安全地修改。
- [ ] 给自己维护的核心模块补上至少一个"特征测试"
- [ ] 检查现有测试是否有"过于脆弱"的问题
第 5 章:介绍重构名录
这一章是重构手法的"使用说明书"。Fowler 为每个重构手法定义了统一的格式:
| 字段 | 含义 | |------|------| | 名称 | 动词短语,如"Extract Function"、"Move Function" | | 动机 | 什么时候需要用这个手法 | | 做法 | 具体的、分步骤的操作指南 | | 示例 | 用代码演示 Before → After |
这种格式后来被广泛借鉴。重构手法的"命名"本身就是一种贡献——当团队里所有人都能用同一套词汇来讨论代码改动时,沟通效率会大幅提升。"我打算对这个函数做 Extract Method"比"我打算把这个代码块提出来变成一个单独的函数"要简洁得多。
第 6-9 章:第一组重构、封装、搬移特性、重新组织数据
这四章是具体的重构手法目录,每章聚焦一个主题:
第 6 章(第一组重构) 是最基础的手法:Extract Function、Inline Function、Extract Variable、Inline Variable 等。这些是重构的"字母表",所有更复杂的重构都是由这些基础手法组合而成的。
第 7 章(封装) 关注信息隐藏:Encapsulate Record、Encapsulate Collection、Replace Primitive with Object 等。核心思想是"不要暴露数据结构的实现细节"。
第 8 章(搬移特性) 关注职责归属:Move Function、Move Field、Extract Class、Inline Class 等。解决的问题是"这段代码/这个字段到底应该放在哪里"。
第 9 章(重新组织数据) 关注数据表达:Replace Magic Number with Symbolic Constant、Encapsulate Variable、Replace Type Code with Class 等。
实战经验:这四章的手法不需要一次全部记住。掌握 Extract Function、Move Function、Rename 这三个基础手法,就能解决 80% 的日常重构需求。
第 10-12 章:简化条件逻辑、重构 API、继承
第 10 章 专门处理条件逻辑的复杂度:Decompose Conditional、Replace Conditional with Polymorphism、Introduce Special Case(也就是 Null Object 模式)、Introduce Assertion 等。条件逻辑是代码复杂度的主要来源之一,这章的手法非常实用。
第 11 章 关注 API 设计:Rename Function、Add Parameter、Remove Parameter、Separate Query from Modifier(查询和修改分离——这是 Fowler 特别强调的原则)等。重构不只是改内部实现,改 API 也是一种重构,而且影响面更大。
第 12 章 处理继承相关的问题:Pull Up Method、Push Down Method、Replace Inheritance with Delegation、Replace Delegation with Inheritance 等。Fowler 在第 2 版中明确表达了对继承的谨慎态度——组合优于继承已经不只是一句口号了。
- [ ] 在下次 code review 中,检查条件逻辑是否可以用多态简化
- [ ] 审视自己的 API 设计,是否有"查询和修改混在一起"的函数
三、关键概念速查
1. 重构(Refactoring)
在不改变软件可观察行为的前提下,改善其内部结构。关键词是"不改变行为"——如果重构改了外部行为,那就不是重构,是改 bug 或者改需求。
2. 代码坏味道(Code Smells)
代码中那些暗示"这里可能有问题"的信号。不是严格意义上的 bug,而是设计和结构层面的问题。包括重复代码、过长函数、过大类、发散式变化、散弹式修改等 20 多种。
3. 测试安全网(Test Safety Net)
为代码编写的自动化测试,目的是在重构过程中快速发现行为被意外改变的情况。和 TDD 的测试不同,重构的测试关注的是"不改坏"而不是"先设计"。
4. 两顶帽子(Two Hats)
Fowler 的核心隐喻:任何时候,你要么在添加新功能(添加新行为),要么在重构(不改变行为,只改结构)。不要同时做这两件事,否则出了问题无法定位。
5. 特征测试(Characterization Tests)
面对没有测试的遗留代码时,先为现有行为编写测试来"锁定"当前行为。这些测试描述的不是"代码应该做什么",而是"代码现在实际在做什么"。有了这个基准线,才能安全地修改。
6. 小步重构(Small Steps)
每次只做一个微小的、可独立验证的改动,然后立刻跑测试。如果测试失败,只需要回退最后一步。这和 Git 的原子提交是同一个思路。
7. 命令查询分离(Separate Query from Modifier)
一个函数要么执行某种操作(有副作用),要么返回某个值(无副作用),不要两者兼有。违反这个原则会导致函数的行为难以预测。
四、核心框架/模型
重构决策流程
发现坏味道
↓
确认有测试覆盖(没有则先写测试)
↓
选一个最小的重构手法
↓
执行一小步
↓
跑测试
↓
测试通过?──是──→ 继续下一步
│
否
↓
回退这一步,分析原因
代码坏味道 → 重构手法映射(部分)
| 坏味道 | 推荐重构手法 | |--------|-------------| | 重复代码 | Extract Function / Extract Class / Pull Up Method | | 过长函数 | Extract Function / Replace Temp with Query / Decompose Conditional | | 过大类 | Extract Class / Extract Subclass / Extract Interface | | 过长参数列表 | Introduce Parameter Object / Preserve Whole Object | | 发散式变化 | Extract Class(按变化原因拆分) | | 散弹式修改 | Move Function / Move Field / Inline Class | | 依恋情结 | Move Function / Extract Function | | 数据泥团 | Extract Class / Introduce Parameter Object | | 基本类型偏执 | Replace Primitive with Object / Introduce Value Object | | Switch 惊悚 | Replace Conditional with Polymorphism | | 冗赘类 | Inline Class | | 夸夸其谈通用性 | Collapse Hierarchy / Inline Function | | 临时字段 | Extract Class | | 过长的消息链 | Hide Delegate / Extract Function | | 中间人 | Remove Middle Man / Inline Function |
重构的"红绿灯"节奏
借用 TDD 的红绿灯隐喻,重构也遵循类似的节奏:
- 绿灯:测试全部通过,可以开始重构
- 小改一步:执行一个微小的重构动作
- 跑测试:确认仍然是绿灯
- 如果变红了,立即回退到上一步的绿灯状态
这个循环的关键在于"小步"——如果每一步都足够小,出错的范围就足够可控。
五、金句摘录
- "任何傻瓜都能写出计算机能理解的代码。优秀的程序员写的是人能理解的代码。"
- "当你发现需要为代码添加注释来解释它在做什么时,先把那段代码提取成一个函数,用函数名代替注释。"
- "代码会随着时间流逝而烂掉。写得再好的程序代码,若是发布了就一直保持原样,照样会风化、破碎乃至分崩离析。"
- "重构的第一步永远相同:确保即将修改的代码拥有一组可靠的测试。"
- "软件的本质特征就是'容易变化'。如果软件不能方便地变化,那它就不能被称为好的软件。"
- "不要为了重构而重构。重构是为了让下一个功能更容易添加、下一个 bug 更容易修复。"
- "当你不确定该不该重构时,问自己一个问题:如果我需要在这里加一个新功能,是会很简单还是会很痛苦?如果答案是后者,那就是该重构了。"
六、行动清单
📅 每天
- [ ] 写代码前先看看有没有"坏味道"可以顺手清理
- [ ] 提交代码前检查:有没有超过 30 行的函数?有没有重复的代码块?
- [ ] 每次提取函数后,给函数起一个能描述"做什么"而不是"怎么做"的名字
📋 每周
- [ ] 至少对一个"让你不舒服"的函数做一次 Extract Function
- [ ] 跑一遍测试覆盖率报告,看看哪些核心路径还没有测试保护
- [ ] 在 code review 中至少指出一个可以重构的地方,并给出具体的重构手法建议
🗓️ 每月
- [ ] 选一个维护最频繁的模块,做一次集中的"坏味道审计"
- [ ] 为一个没有测试的老模块补充特征测试
- [ ] 和团队对齐重构手法的命名——确保大家说"Extract Function"时指的是同一件事
- [ ] 复习一次重构名录,挑 2-3 个之前没用过的手法尝试应用
七、一句话总结
重构不是代码洁癖,而是对未来的投资——今天多花十分钟改好的代码,会省下明天一小时的理解成本和后天一整天的调试时间。
八、读者热议
"重构最大的敌人不是技术难度,而是管理压力。"
很多开发者在评论中提到,书里的手法都很好,但在实际工作中很难执行。原因不是不会,而是没有时间——项目排期紧、deadline 在催、产品经理在催。Fowler 在书里其实也回应了这个问题:重构不是额外的工作,而是正常开发工作的一部分。你不需要专门划出"重构时间",而是在每次修改代码的时候顺便做。但问题在于,很多团队的考核方式不鼓励"没有可见产出"的重构工作。
"第 2 版换 JavaScript 是不是意味着 Java 过时了?"
这个问题在技术社区引发了不少讨论。Fowler 的解释是:选择 JavaScript 是因为它是最容易被理解的动态语言,几乎所有开发者都接触过。这并不意味着 Java 不值得用了。但事实上,从社区反馈来看,很多用 Java/C#/Go 的开发者觉得 JavaScript 示例反而更容易理解——因为动态语言的语法更简单,不涉及泛型、注解等语言特性,注意力可以完全集中在重构手法本身。
"这本书到底该什么时候读?入门阶段还是进阶阶段?"
比较有共识的看法是:至少有 1 年实际编码经验之后再读。太早读,你会觉得"这些我都知道"(其实你不知道,只是没有体会过代码腐烂的痛);太晚读,你会后悔"为什么没早点看"。最佳阅读时机是:你已经维护过至少一个超过 1 万行的项目,并且切实感受到"改代码比写代码难"的时候。这本书不会让你变成架构师,但会让你变成一个更高效的"代码外科医生"。
豆瓣/技术博客读者补充
⭐ 技术博客读者(董泽润)「前五章反复阅读,后五章按需翻阅」
断断续续读完了《重构2》,前五章讲了为什么、什么是、如何做重构,偏理论论证部分,但读起来并不枯燥。后半部分都是具体的重构细节,每章节都有案例。同时这本书定位是改善既有代码,并不涉及如何重写代码(比如用Go重写Python老代码),也不包括重新梳理业务流程。在不改变软件可观察行为的前提下,通过一系列方法调整代码、结构,提高可理解性,降低阅读和修改成本。
认同度:★★★★★。这个阅读策略非常实用。前五章是核心思想(值回票价的部分),后面的重构手法是参考手册(用到时再查)。
⭐ 微信读书读者「重构的第一块基石是测试代码」
Fowler在书中反复强调:没有测试就不要重构。但现实中很多项目根本没有测试,或者测试覆盖率极低。这种情况下如何开始重构?答案不是"先写完测试再重构",而是"先为你要修改的部分写特征测试(Characterization Tests)",然后小步重构。CI+Lint+Peer Review 三件套是团队层面的防护网。
认同度:★★★★★。特征测试是Legacy Code重构中最实用的技巧之一,来自Michael Feathers的《修改代码的艺术》。两者搭配阅读效果最佳。
⭐ 微信读书读者「第2版换JavaScript是明智之举」
很多人质疑第2版用JavaScript替代Java,但作为用Java/C#/Go的开发者,我觉得JavaScript示例反而更容易理解——因为动态语言语法更简单,不涉及泛型、注解等语言特性,注意力可以完全集中在重构手法本身。
认同度:★★★★☆。确实如此。Fowler自己也解释了:选择JavaScript是因为它是最容易被理解的动态语言。这不是在"推荐JavaScript",而是在降低理解门槛。
⭐ 豆瓣读者「这本书不会教你如何写新代码」
这本书的定位非常明确——改善既有代码,而不是设计新代码。如果你想学习如何从零开始设计一个系统,应该去看《设计模式》或《架构整洁之道》。重构的前提是你已经有了一个可以运行的系统,只是它的代码质量在恶化。
认同度:★★★★★。这个区分很重要。重构≠重写≠重新设计。重构是在"保持行为不变"的前提下改善内部结构。
⭐ 微信读书读者「勿以善小而不为——重构不是大事件」
很多人觉得重构是一个"大事件",需要专门的时间、专门的计划、专门的审批。但实际上Fowler提倡的是"持续小步重构"——每次改代码时顺便改善一下附近的小问题。日积月累,代码质量就会保持在一个可接受的水平。这和"童子军规则"异曲同工:让营地比你到达时更干净。
认同度:★★★★★。这才是重构的正确打开方式。不是"憋大招",而是"每天进步一点点"。
笔记生成:2026-04-28 by 喵喵 🐈
相关笔记
同作者仅有此篇 同主题: