《重构》学习总结:改善既有代码的设计,把日课做成习惯
- 约 28 分钟读完 - 全文 10826 字《重构》学习总结:改善既有代码的设计,把日课做成习惯
目标读者:写过一两年业务代码、经常被祖传代码折磨、想搞清楚“重构到底怎么落地”的程序员。
参考书籍:Martin Fowler《重构:改善既有代码的设计(第 2 版)》(Refactoring, 2nd Edition)
阅读目标:建立“重构是刷牙不是大扫除”的日常习惯,把坏味道清单变成自己的启发法清单,学会在安全网保护下小步快走。
0. 写在前面:重构不是大扫除,是刷牙
很多人对重构的想象是这样的:找一个安静的下午,拉上需求方开个会,申请两周“专项重构时间”,然后把某个模块推倒重来。这种理解恰恰是 Fowler 最反对的。
这本书最核心的一个比喻,可以用我自己的话总结为:
重构不是年底大扫除,是每天刷牙。大扫除轰轰烈烈、一年一次、中间家里一直脏;刷牙小额、频繁、不痛不痒,但牙齿几十年不出大问题。
这个比喻对应到工程上有三个推论:
- 小额:一次重构就改一点点,编译能过、测试能绿,马上提交。不是攒一个月再交一个巨型 PR。
- 频繁:每次加功能、改 bug、做 code review 的时候顺手做,融进日常工作流,不需要“立项”。
- 不改变外在行为:这是定义,不是建议。下面马上展开。
为什么这本书值得每个程序员精读?因为坏味道清单就是启发法清单的代码版。算法书教你遇到问题时用什么启发法求解,Fowler 教你看到代码时用什么启发法判断“这里不对劲”以及“该用哪招”。读熟之后,你看代码的眼光会永久改变——这是少数几本读一遍就能立刻涨功力的书。
1. 一句话理解重构:不改变可观察行为的结构调整
Fowler 给出的定义(第 2 版,第 2 章):
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
拆开看有三个关键词:
- 结构调整:动的是代码的组织方式——函数怎么拆、类怎么分、数据放哪里。不是改它“做什么”,是改它“怎么摆”。
- 不改变可观察行为:用户看到的、API 返回的、数据库写入的,统统不能变。你改了行为,那叫“改功能”或“修 bug”,不叫重构。
- 可理解性 / 修改成本:重构的目标从来不是“代码更好看”这种审美追求,而是让下一次修改更便宜。这是经济学决策,不是艺术决策。
1.1 两顶帽子:加功能和重构不同时戴
Fowler 引用了 Kent Beck 的“两顶帽子”比喻,这是全书我最常拿来讲给同事听的概念:
- 戴上“加功能”的帽子:你只添加新代码、新测试,绝不修改已有代码的结构。
- 戴上“重构”的帽子:你只调整结构、保证测试全绿,绝不添加新功能。
写代码的每一刻,你头上只能戴一顶帽子。发现“要加这个功能,得先把这段代码理顺”,正确动作是:摘下加功能的帽子 → 戴上重构帽子 → 重构到方便加功能为止 → 换回加功能帽子 → 加功能。
flowchart TD
A["接到需求:要加新功能"] --> B{"现有代码结构<br/>方便加这个功能吗?"}
B -- "方便" --> C["戴加功能的帽子<br/>写新代码 + 新测试"]
B -- "不方便" --> D["戴重构的帽子<br/>只做结构调整,测试保持全绿"]
D --> E{"结构理顺了吗?"}
E -- "还没有" --> D
E -- "理顺了" --> C
C --> F["提交,帽子归位"]
为什么这么严格?因为同时做两件事,出了 bug 你分不清是谁引入的。重构的每一步都要求测试全绿,如果中途改了行为,红灯亮起时你就失去了判断依据。两顶帽子是重构能保持“小步”的心理学保障:它把一件模糊的、容易失控的事,拆成了两个界限分明的状态。
2. 坏味道清单:程序员的启发法清单
第 3 章是全书的“字典”——24 种坏味道(Bad Smells)。注意 Fowler 的用词是“味道”不是“罪状”:闻到味道不代表必须立刻处理,它只是提示你“这里值得看一眼”。极端的教条主义本身就是一种味道。
24 种味道完整清单如下,后面挑最重要的 10 个深入讲:
神秘命名、重复代码、过长函数、过长参数列表、全局数据、可变数据、发散式变化、霰弹式修改、依恋情结、数据泥团、基本类型偏执、重复的 switch、循环、冗赘的元素、夸夸其谈未来性、临时字段、过长的消息链、中间人、内幕交易、过大的类、异曲同工的类、纯数据类、被拒绝的遗赠、注释。
2.1 神秘命名(Mysterious Name)
味道:变量叫 data、函数叫 process、类叫 Manager,看完名字不知道它是干嘛的。
为什么排第一?因为命名是程序里密度最高的文档。一段代码你可能读十次改一次,名字起得好,十次阅读每次都省时间。Fowler 说得直白:如果想不到好名字,往往意味着你对这段逻辑的职责还没想清楚——命名困难本身就是设计问题的信号。
对应手法:改变函数声明(改名)、变量改名、字段改名。现代 IDE 的重命名重构是安全的,改名字没有借口留到下辈子。
2.2 重复代码(Duplicated Code)
味道:同一段逻辑出现在两个以上的地方。改一处忘另一处,bug 就是这么来的。
这是所有味道里危害最确定、修复手法最成熟的一个。双胞胎函数用提炼函数收拢;兄弟子类里重复的用函数上移;类似但不完全相同的,先拆出相同的部分再收拢。
一个工程界的经典注脚:NASA 的航天飞机软件团队以极低缺陷率著称,他们的经验之一就是极端重视消除重复——任何相似代码都必须解释为什么存在。日常项目虽然不必如此严苛,但“重复三次就提炼”的底线值得守住。
2.3 过长函数(Long Function)
味道:一个函数几十上百行,得靠注释分段才能读懂。
Fowler 的态度非常激进:函数就该短。短函数的好处是通过名字传递意图——calcOutstanding() 比一坨内联计算强得多,因为读者看名字就知道意图,需要细节时才钻进去。
判断标准:不是行数,而是“如果你需要写注释解释一段代码在干嘛,这段代码就该被提炼成函数,函数名用注释里的词”。这个标准极其好用,直接把“注释”这个味道变成了提炼函数的路标。
对应手法:提炼函数(Extract Function)——全书使用频率最高的手法,没有之一。第 2 版里 Fowler 把提炼后的函数长度做到五六行甚至两三行,就是要打破“函数太短不值得”的心理障碍。
2.4 过长参数列表(Long Parameter List)
味道:函数签名里七八个参数,调用处像咒语。
危害有两个:一是调用方每次都要记参数顺序,极易传错(尤其是同类型相邻参数);二是参数多往往说明函数在做不止一件事,或者这些参数本该是一个整体。
对应手法:以查询取代参数(能从参数 A 算出参数 B,就别传 B)、保持对象完整(别从对象里掏三个字段传进去,直接传对象)、引入参数对象(见 2.7 数据泥团)、移除标记参数(布尔 flag 参数往往意味着函数里有个 if 分叉,拆成两个函数更干净)。
2.5 全局数据(Global Data)与可变数据(Mutable Data)
全局数据的味道:任何地方都能读到、改到它,出了问题你找不到是谁改的。单例本质上也是全局数据。
可变数据的味道:一个变量在很大作用域内被反复赋值,你在第 500 行看到的值,无法信任它声明时的样子。
这对兄弟是并发 bug 和“灵异 bug”的头号来源。函数式编程这些年崛起的核心卖点之一就是消灭可变状态,和 Fowler 的判断完全同频。
对应手法:封装变量(把全局变量关进函数里,访问必须经过函数)、拆分变量(一个变量被赋两次值干两件事,就拆成两个)、将引用对象改为值对象(值对象不可变,天然免疫一大类 bug)。
2.6 发散式变化与霰弹式修改(Divergent Change / Shotgun Surgery)
发散式变化:一个类因为不同原因被反复修改。“数据库变了改它,报表格式变了也改它,业务规则变了还改它”——这个类承担了太多职责。
霰弹式修改:一个变化要改很多个类。加一个字段类型,DAO 改、Service 改、DTO 改、前端 mock 改,漏一个就炸。
这俩是同一枚硬币的两面:前者是“多个原因指向一个类”,后者是“一个原因散到多个类”。修复方向相反——发散式变化用拆分阶段、搬移函数把一个类拆开;霰弹式修改用搬移函数、提炼类把散落的逻辑聚到一处。
判断技巧:想象需求来了,你心里默念“要改哪几个文件”。如果答案是“又是这个巨无霸类”,是发散式变化;如果答案是“天哪又是七八个文件”,是霰弹式修改。
2.7 依恋情结与数据泥团(Feature Envy / Data Clumps)
依恋情结:一个函数对另一个类的数据兴趣远大于自己所在类的——函数里 order.getCustomer().getAddress().getCity() 一长串,全是别人家的 getter。治疗方式很直白:搬移函数,让函数搬到它依恋的数据旁边去。“策略”“访问者”模式是例外,它们故意把行为和数据分开。
数据泥团:总是一起出现的三四个字段——startDate/endDate、street/city/zip。它们像连体婴儿一样在各个方法签名和类字段里成群结队。对应手法:提炼类,给这团数据一个家(比如 DateRange、Address)。提炼完常常有惊喜:散落各处的校验、格式化逻辑(依恋情结)会自然地搬进新类。
2.8 基本类型偏执与重复的 switch(Primitive Obsession / Repeated Switches)
基本类型偏执:用字符串存电话号码、用裸 double 存钱、用 int 编码状态。小钱变大钱、状态码幻数满天飞,都是这么来的。对应手法:以对象取代基本类型——一个 Money 类、PhoneNumber 类,把校验和格式化的知识收拢进类型系统。业界血案:金额用 float/double 导致的分分钱对不上账,几乎每个支付团队都踩过,标准做法就是专门的 Money 类型或以最小货币单位为单位的整数。
重复的 switch:同一个类型码的 switch/if-else 链散落多处。每加一种新类型,得把所有 switch 都找出来改一遍——这是霰弹式修改的特化版。对应手法:以多态取代条件表达式(见 3.6 节)。注意 Fowler 的克制:switch 本身不是罪,重复的 switch 才是;如果只有一个 switch 且类型稳定,留着无妨。
2.9 中间人与消息链(Middle Man / Message Chains)
中间人:一个类一半以上的方法都在委托给别的类,自己什么也不干。封装是好事,但封装到这个程度就是纯粹的过路费。对应手法:移除中间人,让调用方直接和被委托对象说话。
消息链:a.getB().getC().getD() 一路要下去,调用方对整条链的结构产生了依赖,链中间任何一环变动都会传导过来。这就是“得墨忒耳定律”(Law of Demeter)说的“只和直接朋友说话”。对应手法:隐藏委托关系。
这一对味道放在一起看很有意思:过犹不及。委托太多是中间人,穿透太多是消息链, Fowler 的词典里没有银弹,只有“闻味道、看剂量”。
2.10 注释(Comments)
注释也上榜了,很多人第一次读会惊讶。Fowler 的逻辑是:
注释本身不是坏味道,它常常是“ deodorant(除臭剂)”——用来遮盖代码本身的臭。
当你觉得“这里得写段注释解释一下”,先问:能不能提炼函数用函数名说清楚?能不能改变量名说清楚? 能,就别写注释。注释该写的是“为什么这么做”(业务背景、历史坑、外部约束),而不是“这段代码在干嘛”——后者是代码没写清楚的忏悔。
2.11 其余味道速查表
| 味道 | 一句话识别 | 首选手法 |
|---|---|---|
| 循环(Loops) | for 循环里干的事能用管道表达 | 以管道取代循环(filter/map/reduce) |
| 冗赘的元素(Lazy Element) | 这个函数/类就只有一层转发,毫无增量 | 内联函数、内联类 |
| 夸夸其谈未来性(Speculative Generality) | “为将来预留”的抽象,现在只有一个实现 | 折叠继承体系、内联 |
| 临时字段(Temporary Field) | 某个字段只在特定算法运行时才有值 | 提炼类 + 搬移函数,给它一个家 |
| 内幕交易(Insider Trading) | 两个模块私下大量交换数据,耦合紧密 | 搬移函数/字段,减少私聊 |
| 过大的类(Large Class) | 一个类字段太多、职责太多 | 提炼类、提炼超类 |
| 异曲同工的类(Alternative Classes) | 两个类接口不同干的事一样 | 改变函数声明 + 搬移函数统一接口 |
| 纯数据类(Data Class) | 只有字段和 getter/setter,没有行为 | 把操作它的行为搬进来(封装记录) |
| 被拒绝的遗赠(Refused Bequest) | 子类继承了一堆用不上的东西 | 函数/字段下移,或以委托取代继承 |
“夸夸其谈未来性”值得单独敲一下黑板:YAGNI(You Aren’t Gonna Need It)。每个“将来可能用得上”的抽象,都是要付利息的技术债——它现在就开始收阅读成本,而兑现的那一天往往永远不会来。
3. 核心重构手法:七招走天下
第 6 章起的重构名录有 60 多个手法,但日常高频使用的集中在少数几招。下面七招是我个人使用频率最高、也最能体现“小步安全”思想的。
3.1 提炼函数(Extract Function)
场景:一段代码需要注释才能看懂;或者一个函数太长。
口诀:意图与实现分离——“这段代码做什么”(意图)交给函数名,“怎么做”(实现)交给函数体。
小步版本(Fowler 的名录里每个手法都有这种机械化步骤,这是本书最容易被低估的价值):
- 创建新函数,按意图命名(想不到好名字就先叫临时名,回头再改)。
- 把要提炼的代码复制过去(是复制,不是剪切——先留着原件,错了好回退)。
- 检查局部变量:被新函数读到的 → 传参;被修改的 → 尽量变成返回值,一次只返回一个。
- 编译。在源位置替换为对新函数的调用。
- 测试。全绿则提交。
避坑:一次提炼只处理“只读变量多、被改变量少”的块;被改变量超过一个时,先用“拆分变量”或调整结构再提炼。
3.2 内联函数(Inline Function)
提炼的反操作。场景:函数体已经和函数名一样清楚(getRating() { return moreThanFiveLateDeliveries ? 2 : 1; } 改名后可能直接内联掉),或者一堆小函数间接层太深、改名/重组前需要先“摊平”。
重构名录里的手法大多成对出现(提炼/内联、搬入/搬出、封装/暴露),这不是凑数——可逆性是小步快走的前提:这步走错了,反手一步就能退回来。
3.3 搬移函数(Move Function)
场景:函数引用别的上下文的元素多于自己所在的上下文(依恋情结的特效药);或者模块化需要重新划分边界。
口诀:逻辑和它操作的数据住在一起。搬移时先检查函数引用的所有东西——它们要么一起搬,要么变成参数。搬完旧位置可以留一个委托函数过渡,等调用方都改完再删。
3.4 提炼变量(Extract Variable)
场景:表达式又长又复杂,比如:
return order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100);逐个起名字:
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;零风险、几秒钟、可读性立刻翻倍。这是性价比最高的重构手法,新人最值得先养成这个习惯。变量命名还能顺带自我解释业务规则:原来那 0.05 是批量折扣,0.1 是运费比例。
3.5 封装记录(Encapsulate Record)
场景:代码里到处传裸的 object/map/dict(纯数据类的输入版),字段名靠口口相传。
把记录变成类,访问走方法:字段名写错编译期就炸,而不是线上拿到 undefined;还能给计算字段提供方法(total 不必存,用 get total() 算)。这是 TypeScript 兴起后整个行业都在做的事——让类型系统替你站岗。
3.6 以多态取代条件表达式(Replace Conditional with Polymorphism)
场景:同一个类型码的 switch 散落多处(重复的 switch 味道);或者一个函数内部按类型分叉,分支逻辑还会持续增加。
步骤:为每种类型建子类 → 把条件分支的逻辑搬成子类的覆写方法 → 原函数变抽象。书里的经典例子是“鸟类评级”:按鸟的种类(欧洲燕子、非洲燕子、挪威蓝鹦鹉)分支计算飞行速度和评级,重构后每种鸟一个子类,新增鸟种只需新增一个类——修改从“找到所有 switch”变成“新建一个文件”,这正是开闭原则。
避坑:类型码稳定且数量少时不要硬上多态。switch 只有一处、类型十年不变,多态带来的类爆炸是另一种过度设计。再强调一次:闻味道,看剂量。
3.7 拆分阶段(Split Phase)
场景:一段代码同时干两件性质不同的事——典型的如“解析输入 + 业务计算 + 输出格式”。
把两段拆开,中间用一个明确的数据结构传递:第一阶段产出中间对象,第二阶段消费它。好处是两个阶段的修改互不影响——换输出格式不动计算逻辑,换输入来源不动下游。书中 statement 案例(下节)的核心一步就是拆分阶段。命令行工具、编译器(解析→语义分析→代码生成)、数据管道,全是这个手法的工业级应用。
4. 全书导读案例:statement 计费重构走查
第 2 版第 1 章用 JavaScript 完整走查了一个案例,非常值得复现一遍,因为它是“小步快走”的教科书级演示。
原始代码:一个剧团的账单系统,statement(invoice, plays) 输出客户看戏的账单文本。一个约 40 行的函数,里面一个 switch 按戏剧类型(悲剧/喜剧)算钱,外加积分计算和字符串拼接,全部揉在一起。
需求来了(这就是重构的触发器):① 账单要支持 HTML 输出;② 剧团打算演更多类型的戏(比如“历史剧”“田园剧”),算钱和积分规则要可扩展。
Fowler 没有直接动手写 HTML 版本,而是先重构。整个过程的关键步骤:
- 先补测试:原代码没有任何测试。第一步不是改代码,是给现有行为写一批测试,把输出钉死。
- 提炼函数:把 switch 算钱的部分提炼成
amountFor(aPerformance),把积分计算提炼成volumeCreditsFor()。每提炼一个,跑一次测试。 - 提炼变量 + 改名:
thisAmount这种模糊名改成result,消除不必要的中间变量。 - 拆分阶段:把“计算账单数据”和“渲染文本账单”拆成两个阶段,中间用一个数据结构传递。此时
statement()变成:const data = createStatementData(invoice, plays); return renderPlainText(data);。 - 加功能:此时写
renderHtml(data)只消费中间数据,计算逻辑一行不碰——HTML 版本轻松加上。 - 以多态取代条件表达式:为“悲剧计算器”“喜剧计算器”建子类,switch 消失。加新戏种 = 加一个新子类。
flowchart LR
A["原始代码<br/>一个 40 行大函数<br/>无测试"] --> B["补测试<br/>钉死现有行为"]
B --> C["提炼函数<br/>amountFor / volumeCreditsFor"]
C --> D["拆分阶段<br/>计算数据 vs 渲染文本"]
D --> E["中间数据结构<br/>statementData"]
E --> F["加功能:renderHtml<br/>复用同一份数据"]
C --> G["以多态取代条件表达式<br/>TragedyCalculator<br/>ComedyCalculator"]
G --> H["加功能:新戏种<br/>只需新增子类"]
F --> I["最终结构<br/>好改、好测、好扩展"]
H --> I
这个案例的精髓不在于手法本身(都不难),而在于节奏:40 行的小函数,Fowler 也花了几十个小步、每步跑测试。他要演示的是:重构的安全性不来自你的技术,来自你的步幅。步幅足够小,每一步都可以验证,出错最多浪费一步。
5. 重构与测试:先补测试,再动刀
第 2 版相对第 1 版(1999 年)最大的时代变化,就是自动化测试从“推荐”变成了“前提”。Fowler 的态度很明确:
没有测试保护的重构不叫重构,叫“改代码并祈祷”。
逻辑链条是这样的:
- 重构的定义是“不改变可观察行为”——那你怎么知道行为没变?只能靠测试。
- 重构要求小步快走——每一步都要验证,人工验证一遍要十分钟,自动化只要十秒。没有自动化,小步走不起来。
- 面对祖传代码没有测试怎么办?先补测试再动刀。补测试本身常常需要先轻量重构(比如提炼函数让依赖可以 mock),这就回到了两顶帽子:戴着测试帽做最小限度的结构调整,只为让测试能跑起来。
Michael Feathers 在《修改代码的艺术》(Working Effectively with Legacy Code)里把“没有测试的代码”直接定义为遗留代码,和 Fowler 互为犄角。两本可以搭配读:Fowler 教你“怎么改”,Feathers 教你“改不动时怎么办”。
flowchart TD
A["要动一段代码"] --> B{"有测试覆盖吗?"}
B -- "有" --> C["跑一遍,确认全绿"]
B -- "没有" --> D["最小限度补测试<br/>(为可测性做的微调也算)"]
D --> C
C --> E["做一小步重构"]
E --> F{"测试全绿?"}
F -- "绿了" --> G["立即提交<br/>(小步提交 = 免费回滚点)"]
F -- "红了" --> H["撤销这一步<br/>或立刻修复,不要带着红灯继续"]
G --> E
H --> E
E --> I{"目标达成?"}
I -- "没有,继续小步" --> E
I -- "达成" --> J["收工:行为没变,结构更好"]
这张“安全网流程图”里最反直觉也最重要的一条是:测试红了,默认动作是撤销这一步,而不是往前调。小步走意味着你刚刚那一步的范围是已知的、可控的,退回上一步的代价几乎为零;带着红灯继续走,错误范围就不可控了。配合小步提交(每步都 commit),你就拥有了一条随时可以回滚的时间线。
6. 重构的时机与节奏:融进日常,而不是立项
6.1 三次法则与“顺手重构”
Fowler 给的实用规则:
- 事不过三,三则重构(Rule of Three,源自 Don Roberts):第一次写就写,第二次重复捏着鼻子忍,第三次必须提炼。
- 加功能前重构:代码结构碍事,先重构到“加这个功能很容易”,再加功能(两顶帽子)。
- 修 bug 时重构:bug 之所以藏得深,往往因为代码难懂。把 bug 修掉的同时,把“让 bug 藏不住”的结构改出来。
- code review 时重构:看到别人的坏味道,当面演示一次提炼函数,比留十句评论有用。
6.2 破窗效应与童子军军规
犯罪学里的“破窗效应”:一栋楼有几扇破窗没人修,很快所有窗户都会被砸——混乱会邀请更多混乱。代码库一模一样:第一处“先这样吧,回头再改”开了头,后面所有人都会照着最烂的那段代码的标准来写。
对策是 Bob Martin 普及的童子军军规(Boy Scout Rule):
离开营地时,要让它比你来时更干净。
落到代码上:每次提交,都顺手改进一点点——改一个烂名字、提炼一个长函数、删掉一段死代码。不需要翻天覆地,只要持续为正。一个团队做到这点,代码质量会随时间收敛而不是发散。这是对付“祖传代码”唯一被验证过可持续的策略:不是一次性手术,是持续的免疫反应。
6.3 什么时候不重构
书里同样重要的克制:
- 代码又烂又永远不用改——让它烂着,重构它没有回报。
- 重写比重构便宜——偶尔成立,但门槛极高,见下节的 Netscape 教训。
- 离截止日期太近且重构不能帮上眼前的忙——重构的回报在下一次修改,先交付,记技术债,回头还。
记住:重构是经济学决策。每一步都应该能回答“这能让接下来的修改更便宜吗?”答不上来的重构是自我感动。
7. 大规模重写的教训:Netscape 之死
聊到重构,绕不开软件工程史上最著名的一次“我们不重构了,我们重写”的惨案。
背景:Netscape Navigator 曾统治浏览器市场,但代码库在快速迭代中积重难返。1997 年前后,Netscape 决定抛弃旧代码,从头重写浏览器引擎(即后来的 Gecko / Mozilla 项目)。
结果:重写花了将近三年。这三年里 Netscape 几乎没有发布有竞争力的新版本——6.0 版本直到 2000 年底才问世。与此同时,微软的 IE 持续迭代、免费捆绑 Windows,市场份额被彻底翻盘。等 Mozilla 成品出来时,浏览器战争已经结束了。
Joel Spolsky 在 2000 年那篇著名文章《Things You Should Never Do, Part I》里拿 Netscape 当反面教材,结论斩钉截铁:
重写是你能对软件做的最糟糕的战略性错误,没有之一。
他的论证值得背下来:
- 旧代码不是垃圾,是知识的化石。那些丑陋的补丁,每一个都对应一个真实用户踩过的坑、一个边界情况。重写时你看不见这些知识,于是用户会在新版本里重新踩一遍所有的坑。
- 重写期间你无法交付。老代码烂归烂,它在生产环境里跑着、赚着钱;新代码在成型之前一文不值。竞争对手不会等你。
- 重写通常伴随着“这次一定要设计完美”的野心——这正是“夸夸其谈未来性”的企业级版本,工期必然失控。
Fowler 在第 1 版就引用过这个故事来论证:重构的价值恰恰在于它是重写的替代品。用持续的小步重构,让烂代码在“一直保持可交付”的状态下逐步变好,是风险唯一可控的路径。
当然要有公允的注脚:后来 Mozilla 代码确实成了 Firefox 的底子,长期看那次重写并非全无产出。但作为商业决策,Netscape 公司没活到收获的那天——这正是 Joel 论点最残酷的部分。
8. 带走的清单
8.1 核心心智模型(五句话)
- 重构 = 不改变可观察行为的结构调整;目标不是美,是让下次修改更便宜。
- 两顶帽子:加功能和重构永不同时戴。
- 坏味道清单 = 启发法清单:闻到味道 → 查名录 → 用对应手法。味道是提示,不是罪状。
- 刷牙,不大扫除:小步、频繁、顺手;童子军军规,每次提交留代码更干净一点。
- 先补测试,再动刀;测试红了先撤销,不带着红灯往前走。
8.2 新人避坑指南
- 坑一:把重构当借口拖延交付。 重构是加功能的加速器不是挡箭牌。每次重构前问自己:这步能让手头这个需求更快落地吗?
- 坑二:没有测试就动祖传代码。 先补测试。补测试时发现代码不可测,先做最小限度的可测性重构(提炼函数、依赖注入),别一步到位。
- 坑三:PR 里混着“重构 + 新功能 + 修 bug”。 reviewer 无法区分行为变化来自哪里。拆 PR,或者至少拆 commit。
- 坑四:看到 switch 就上多态,看到简单类就套设计模式。 夸夸其谈未来性也是坏味道。YAGNI。
- 坑五:迷信“等我有整块时间再重构”。 那一天永远不会来。能融进日常的小重构才有复利,立项式重构大概率变成 Netscape 式豪赌。
- 坑六:改名字嫌麻烦。 IDE 的安全重命名几秒钟一次,这是全软件业性价比最高的操作,没有之一。
8.3 一页速查:味道 → 手法
mindmap
root((坏味道<br/>→<br/>重构手法))
代码冗余
重复代码 → 提炼函数
过长函数 → 提炼函数
过长参数列表 → 引入参数对象
数据泥团 → 提炼类
冗赘元素 → 内联函数
职责错配
依恋情结 → 搬移函数
发散式变化 → 拆分阶段
霰弹式修改 → 搬移函数聚拢
内幕交易 → 搬移函数减少私聊
过大的类 → 提炼类
类型与状态
基本类型偏执 → 以对象取代基本类型
重复的switch → 以多态取代条件表达式
全局数据 → 封装变量
可变数据 → 拆分变量/值对象
临时字段 → 提炼类
意图不清
神秘命名 → 改名
注释 → 提炼函数让代码自解释
复杂表达式 → 提炼变量
消息链 → 隐藏委托关系
中间人 → 移除中间人
9. 写在最后
《重构》这本书最被低估的一点:它表面上是一本“手法名录”,实际上是一套把代码质量从“良心活”变成“机械活”的方法论。坏味道给你嗅觉,名录给你机械步骤,测试给你安全网,两顶帽子给你节奏——四样东西配齐,重构就从“需要勇气和天赋的大工程”变成了“不需要下决心就能做的日常小事”。
而日常小事的复利是惊人的。每天让代码干净 1%,一年后它和自然腐化的代码库就是两个物种。这本书 1999 年出第 1 版、2018 年出第 2 版,横跨二十年依然是软件工程必读,原因无他:代码会腐化是永恒规律,而刷牙永远有效。