20|最常用的三个动作:提炼函数、内联、改名字
如果只让你练三个重构手法,应该选哪三个?为什么是这三个?
一、先说结论
福勒给出的答案很朴素:提炼函数(Extract Function)、内联(Inline)、改名(Rename)。
- 提炼函数 是最基础、最高频的重构。把一段逻辑抽出来、给它起个名字,几乎所有其他手法最终都要用到它。
- 内联 是提炼的逆操作。当一个间接层没有价值、或者抽象过度时,把它塞回去,让代码更直接。
- 改名 看起来最简单,却是提升可读性性价比最高的动作。名字本身就是最便宜的文档。
掌握这三个动作,就能解决日常开发中相当大一部分结构问题。而且它们彼此配合:抽、塞、名,构成一个可以反复使用的基本循环。软件工程界普遍认为,能熟练使用 IDE 的一键 Extract / Inline / Rename,是重构入门最重要的基本功。
二、我们先理解一个问题
前面我们已经有了手法全景图:提炼、内联、搬移、封装、重组数据、简化条件。
可现实中,一个人一天写的代码,根本用不到那么多种类。大多数时候,他心里冒出的念头其实就是两句:
那么问题来了:如果只练几个动作,哪几个的投入产出比最高? 为什么福勒会把「改名」这种看起来毫无技术含量的动作,和「提炼函数」这种核心手法并列?
三、作者到底提出了什么观点?
福勒在手法的编排里,花了相当大的篇幅讲 Extract Function,并且把它当作很多其他手法的第一步:你想搬移代码,往往先提炼再搬;你想简化条件,往往先被提炼出来才谈得上简化。可以说,没有提炼函数,后面很多手法都无从落地。
Inline 则常被当成「次要的」。但福勒把它明确列为独立手法,因为它解决一种真实而常见的病:过度抽象。不是所有抽出来的东西都值得保留,有些函数名和函数体一样清楚,甚至更啰嗦;有些中间层是历史遗留,早就不再有价值。这时候正确的动作不是继续往上堆,而是塞回去。
Rename 是三者中最容易被轻视的。但福勒一贯强调命名的分量——他甚至把「注释」列为坏味道,理由正是:当你能用一个好名字表达意图时,注释就是多余的。 改一个准确的名字,等于给代码写了一份永不过期、永远不会忘记更新的文档。
三者合起来构成一个循环:Extract 把模糊的一坨变成有名字的一块;Inline 把没有意义的名字去掉;Rename 把名字调整到准确。 名字,是这个循环的核心。
四、把这个观点翻译成人话
想象你在写文章。
初稿的时候,你常常是「一口气写一大段」——因为思路是连续的,先把想法倒出来最重要。这时候这一段读起来又长又闷。
接下来你会做三件事:
- 拆分:把长段拆成几个小段,每段只讲一件事,各起一个小标题。这就是 Extract Function——把一坨逻辑变成几个有名字的单元。
- 合并:有些话说得太碎,单独成段反而啰嗦,就把它们并回一段。这就是 Inline——把没有价值的拆分收回来。
- 改标题:小标题起得不好,读者抓不住重点,就换一个更准确的。这就是 Rename——让名字准确表达内容。
写文章和写代码,在这里是同一件事:先写成一坨,再拆开、合并、命名,直到每部分都名实相符。 一个有经验的作者和一个有经验的程序员,做的都是同一套动作,只是操作对象不同。
五、为什么会这样?
把这三个动作的配合关系画出来,是这样:
graph TD
A[一段逻辑混在一起, 意图不清] --> B[Extract Function: 抽出来并命名]
B --> C{抽出来的东西有价值吗?}
C -->|名字清楚, 职责单一| D[保留]
C -->|名字没意义, 只是搬运| E[Inline: 塞回去]
D --> F[Rename: 让名字准确表达意图]
E --> F
F --> G[代码变得可读、可理解]
G --> H[后续改动更容易、更安全]
这条链的关键在于两头都能走。
很多人对重构的误解是:只会「抽」。结果代码被拆成几十个小函数,读一行要跳转五次,反而更难懂。福勒把 Inline 单独列出,就是在提醒:抽象是有成本的,过度抽象也是一种坏味道。 该抽的时候抽,该塞的时候塞,方向由「能不能让代码更清楚」决定,而不是由「抽了显得更专业」决定。
而 Rename 之所以最关键,是因为整条链最终都要落到「名字」上:提炼的价值靠名字兑现,内联时你判断「名字有没有意义」,也是靠名字。名字不对,前面两步就白做。
六、举一个现实生活中的例子
看一段真实会遇到的重构,用极短的代码演示提炼函数。
改之前,一个方法里既有计算、又有打印、又有格式拼装,全搅在一起:
void printOwing() {
double outstanding = 0;
for (Order o : orders) {
outstanding += o.amount();
}
System.out.println("***********************");
System.out.println("**** Customer Owes ****");
System.out.println("***********************");
System.out.println("name: " + name);
System.out.println("amount: " + outstanding);
}
用 Extract Function 抽成几块之后:
void printOwing() {
double outstanding = calculateOutstanding();
printBanner();
printDetails(outstanding);
}
double calculateOutstanding() { /* 遍历累加 */ }
void printBanner() { /* 打印星号横幅 */ }
void printDetails(double outstanding) { /* 打印姓名与金额 */ }
现在,读 printOwing 这一层,你一眼就知道它做三件事:算、印头、印明细。细节被名字挡住了,但不是被藏起来——需要时点进函数就能看到。
什么时候该 Inline? 如果团队里有个方法长这样:
boolean moreThanFiveDeliveries() {
return deliveries > 5;
}
它的名字和它的实现一样清楚,甚至更啰嗦,调用处还得跳一次。这种情况下,把它内联回调用点,代码反而更直接。同样,如果某次重构留下了一个纯粹转发、毫无加工的中间函数,那也是内联的典型目标。
Rename 的价值则更日常:把 calc() 改成 calculateShippingFee(),把循环变量 d 改成 dueDate,把 status == 3 背后的枚举改成 OrderStatus.PAID。改完之后,你不需要任何注释,就能读懂这行代码在干什么。而且这些改动在 IDE 支持下一键完成,几乎零风险——行为完全没变。
七、回到书中重新理解
再回看福勒对这三个动作的强调,就顺理成章了。
他并不是说其他手法不重要,而是说:这三个是所有手法的「原子动作」。 提炼函数给出了「命名」这个能力,内联保住了「抽象不能过度」的平衡,改名则直接命中了他最在意的一件事——代码应当自解释。
这一点和第 15 篇讲的「注释是坏味道」是一脉相承的:福勒不是反对注释,而是主张能用名字说清楚的,就别用注释说。因为注释会过期,名字(在工具支持下)可以随时更新。改名,就是把这份「活的文档」不断校准。
八、这个观点背后还有什么?
第一,这三个动作恰好构成了「抽象」的完整循环:加一层(Extract)、减一层(Inline)、校准一层(Rename)。 只学抽不学塞,就容易走进「越抽象越乱」的坑。
第二,它们都特别依赖工具。 现代 IDE 对 Extract、Inline、Rename 的支持已经非常成熟,一键完成、自动更新所有引用。也正因如此,这三个动作的执行成本极低,是性价比最高的日常习惯。福勒当年需要手工小心的操作,今天几秒钟就能做完。
第三,它们和「小步前进」是天作之合。 每一个动作都足够小,都能在工具支持下保持行为不变,改完就能跑测试确认。高频 + 零风险 + 立即验证,正是这三个动作值得反复练的原因。
九、这个观点有问题吗?
十、今天再看这个观点
(以下为现代延伸,非福勒原文)
IDE 与语言服务器已经把这三个动作变成了基础设施。 一键 Extract、一键 Inline、一键 Rename 几乎出现在所有主流开发环境里。今天一个开发者在键盘上敲几次快捷键完成的工作,正是福勒当年在书里逐条写下操作步骤的内容。工具的普及,本身就是这套方法被行业接受的证据。
静态类型与编译器进一步放大了改名的价值:当名字、类型都准确时,编译器能替你把住很多关口。这也是为什么在强类型语言里,改名常常是「最安全却收益最大」的重构。
AI 辅助编程则带来了一个新情况。模型很擅长做提炼、内联、改名这几件事——它生成的代码往往「看起来很规范」。但最需要人判断的恰恰是名字:processData、handleInfo、utilMethod 这类空洞命名,AI 很容易大批量地造出来。它能让动作变快,却不能替你判断这个名字是否真的表达了领域含义。 越是能自动改代码,命名这件事就越需要人来把关。
十一、这一篇真正应该记住什么?
-
只练三个:Extract、Inline、Rename。 它们是所有重构手法的原子动作,覆盖日常大部分结构问题。
-
提炼函数是第一基本功。 它把模糊的一坨变成有名字的单元,也是许多其他手法的第一步。
-
内联是提炼的逆操作,专治过度抽象。 记住抽象有成本,不是越抽越好。
-
改名是性价比最高的动作。 名字就是最便宜、最不容易过期的文档;在自有代码里,它几乎零成本。
-
三者构成「抽—塞—名」的循环。 方向由「是否更清楚」决定,而不是由「是否更抽象」决定。
十二、留一个问题
我们从第 16 篇走到这里,其实一直在默认一件事:你有自动化测试可以随时跑,能独立提交,也能放心地改动别人写的代码。
可是现实中的团队,真的都具备这些前提吗?如果测试没人写、构建要等很久、动别人的代码被视为「冒犯」,那么前面讲的所有手法和节奏,还成立得起来吗?
福勒这本书表面上讲的是技术动作,底层却悄悄依赖了一整套团队工程实践。
下一篇,我们就来看看重构的隐藏前提——为什么它是一种「团队工程」,而不是个人洁癖。