跳到主要内容

一场手术看全程:重构是什么

1. 这一章讲什么

原书不用定义开书,而是直接做一场手术:一个戏剧演出团的账单程序,功能正常,但写成了一个大函数。作者当着读者的面,用一串小改动把它变干净——每一步都小到几乎无聊,每一步做完都跑一遍测试。读完这一章你会知道:重构的动作长什么样、节奏是什么、为什么每步都要那么小。定义和原则留给后面的章节;这一章只负责让你「见过」。

2. 顶层全景:要动的那个程序

一家戏剧演出团给客户发账单:客户订了几出戏,剧团按剧目类型观众人数收费,再按人数发「观众量积分」当折扣。数据存在两个文件里,程序读进来,打印一张文本账单1

plays.json(剧目表) invoices.json(账单)
hamlet → 悲剧 BigCo: hamlet 55 座
as-like → 喜剧 as-like 35 座
othello → 悲剧 othello 40 座
└────────── statement 函数 ──────────┘
│ 44 行,一个大循环,里面塞着
│ 计费 switch + 积分累计 + 拼字符串

"Statement for BigCo
Hamlet: $650.00 (55 seats) ... Amount owed is $1,730.00
You earned 47 credits"

图说:计费、积分、排版三件事挤在同一个函数里——这正是后面所有改动要拆的东西。

计费规则本身不长。金额以美分计:悲剧基础 40000 美分,超过 30 座每座加 1000;喜剧基础 30000 美分,超过 20 座先加 10000 再每座加 500,另外每座再加 300。积分:每场 max(人数−30, 0),喜剧再多加人数除以 5 取整。拿真实数据核一遍:hamlet 55 座 → 40000 + 1000×(55−30) = 65000 美分 = $650.00;as-like 35 座 → 30000 + 10000 + 500×15 + 300×35 = 58000 = $580.00;othello 40 座 → 50000 = $500.00。合计 173000 美分,输出 $1,730.00;积分 25 + 12 + 10 = 47。这三行输出和 47 个积分都是原书给出的真实运行结果,后面每一步改动都要守住它们不变1

3. 为什么要动它:两个马上要来的需求

程序能跑,评论只有一句「组织不甚清晰」。放在几百行的真实系统里,这样的函数意味着:想改的时候找不到改的地方,一改就错。作者给出的行动法则写成了箴言:要加特性而代码不好加时,「那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性」2

接下来真的来了两个需求:一,账单要出 HTML 版;二,剧团要尝试更多剧种,计费和积分规则必改——作者的判断是「他们一定会在6个月之内再次修改它」3。如果为了 HTML 直接复制一份函数,将来每次改计费就得改两处并保证一致。所以先动结构。注意作者的限定:「是需求的变化使重构变得必要。如果一段代码能正常工作,并且不会再被修改,那么完全可以不去重构它」4

4. 主走查:这场手术的全程

动第一刀之前先做一件事:给 statement 配上自我检验的测试——拿几张账单跑函数,把输出和人工核对过的字符串(一段字符文本)比对,红了就是改坏了。这套网怎么搭是第 04 章的主题,这里只交代它的存在;没有它,后面每一步都走不稳。

第一刀:提炼计费函数。 循环中间那段 switch 一眼能看出「在算一场演出的费用」,但作者提醒:这种理解只是脑子里「转瞬即逝的灵光」,得把它搬回代码里5。搬法是把这段代码抽成独立函数 amountFor,按它「做什么」命名。抽之前要清点变量:被用的(perf、play)变成参数(传给函数的输入值)传进去,被改的(thisAmount)变成返回值带出来。改完立刻跑测试——绿了就提交。这一步用的手法叫提炼函数(把一段代码抽成一个有名字的新函数,原处改为调用它)。

箴言:重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。6

第二刀:改名。 thisAmount 改成 result(作者的习惯:函数返回值一律叫 result),参数改成 aPerformance——在 JavaScript 这种没有静态类型检查的语言里,把类型写进参数名,读的人不用猜7

第三刀:去掉临时变量 play。 play 是从 perf 算出来的,没必要作为参数传来传去。先提炼一个查询函数 playFor(perf),再把 play 变量内联掉,最后用「改变函数声明」把参数从 amountFor 的签名上摘掉。这时警钟响起:原来每次循环只查一次剧目,现在要查三次。作者的处理是:先不管。「大多数情况下可以忽略它。如果重构引入了性能损耗,先完成重构,再做性能优化」8。理由在 12 章还会展开:性能只集中在少数热点上,而结构好的代码回头调优容易得多。

第四刀:同样手法清理积分和格式化。 积分累计提炼成 volumeCreditsFor;格式化函数从临时变量 format 变成真函数,改名 usd,顺手把「除以 100」(美分转美元)也搬进去——用整数美分存钱是常见做法,避免浮点数存货币出错。

第五刀:移除累加变量 volumeCredits。 这个变量在循环里累加,不好直接抽。套路是四小步,每步都编译、测试、提交:①拆分循环,把积分累加单独成一个循环;②移动语句,把声明挪到循环旁边;③把循环提炼成函数 totalVolumeCredits;④内联变量,彻底删掉中间变量。totalAmount 照方抓药,插曲是:函数最好就叫 totalAmount,可名字被变量占着,于是先给函数随便起名 appleSauce,等变量删了再改回来——名字冲突就这样绕过去。

中盘小结(原书第一个节点): 顶层 statement 只剩 7 行,打印是打印、计算是计算。这是原书说的三个节点中的第一个:把大函数分解成一组小函数9

第六刀:拆分阶段。 新需求(HTML 版)要复用计算逻辑,但现在所有小函数都嵌套在文本版函数里。办法是把整段逻辑劈成前后两截:前一段 createStatementData 只算账,产出一个中转数据结构(两个阶段之间传递数据的容器);后一段只管把数据渲染成文本或 HTML。填充中转结构时每场演出被「加料」成 {play, amount, volumeCredits},用的是浅拷贝——「我总是尽量保持数据不可变」,可变状态是烫手山芋10。拆完之后,HTML 版只需 17 行渲染代码,一行计算逻辑都不用复制。这是第二个节点:计算与排版分离。

第七刀:多态计算器。 第二个需求(新剧种)落在计费分支上。现在的 switch 每加一个剧种就多一条 case,散落两处。做法:先把计费和积分函数搬进一个类 PerformanceCalculator(第三个节点:按类型组织计算),再建悲剧/喜剧两个子类,把 switch 的每个分支下沉到对应子类里;用一个工厂函数按 play.type 决定造哪个子类——JavaScript 的构造函数没法返回子类,所以必须经过这层普通函数。超类里的 amount 只剩一行 throw new Error('subclass responsibility')(子类的责任):谁来继承谁负责实现。积分计算的通用部分 max(座−30, 0) 留在超类,喜剧子类加一点再调超类方法。

终盘。 代码从 44 行变成 70 行,「这主要是将代码抽取到函数里带来的额外包装成本」——行数涨了,但可读性和可改性换回来了11

5. 作者的判断与证据

判断证据/性质
好代码有客观标准:「好代码的检验标准就是人们是否能轻而易举地修改它」12作者立场,全书反复论证;不是美学偏好
小步更快:步子小+每步可工作,出错时只需看一小段作者 20 年实践;Kent Beck 现场演示给他同样的震撼13
性能异议先搁置基于软件性能只集中于少数代码的经验(原书 2.8 节,本拆解第 12 章讲)
营地法则:离开时比来时更健康作者对「没时间重构」的标准回答14

判断(我们的,不是书里的): 这个示例最值钱的不是手法清单,而是它演示了「理解」与「代码」的互相喂养——先提炼才能看清结构,看清结构才发现下一处该改哪里。原书把它叫「积极正向的反馈环」。如果错,会错在: 如果读者拿到的代码本身写得极好,这个反馈环的收益会趋近于零——重构的收益与代码现状的糟糕程度成正比,这一点原书没有明说。

6. 边界与局限

  • 示例太小,不配重构。 作者自己承认:这个小程序「根本不值得我们这么做」,请读者把它想象成大系统的一部分15。真实系统里变量更多、步骤更碎。
  • JavaScript 是展示载体,不是推荐语言。 「这不是一本“用JavaScript进行重构”的书,而是一本关于重构的通用书籍」16;JS 特有的重构(回调改 promise 等)不在书里。与 Java 版的可见差异:JS 无编译期检查(「编译」一词在这里泛指运行前的所有准备步骤)、支持嵌套函数从而省掉传参、构造函数不能返回子类所以多态要经过工厂函数。这些差异影响的是手法的步骤细节,不是手法本身。
  • 本章所有数字($650.00、47 credits、40000 美分)除 44→70 行的行数对比外,全部直接来自原书示例数据,不是编造。

7. 可带走的

  1. 重构的触发器是「需求要来了,而代码不好改」,不是「代码丑」。
  2. 每一步:改动小到能一眼检查 → 跑测试 → 提交;步子大了出错,回滚重走更小的步。
  3. 抽函数前先清点变量:用的变参数、改的变返回值;被赋值太多次就先别抽。
  4. 临时变量是提炼的天敌;能变成查询函数的就变成查询函数。
  5. 复用先拆阶段:算账的一段、排版的一段,中间用数据结构接头。
  6. 多个函数按同一套类型分支时,把分支收敛(聚拢到一处)到一个工厂函数 + 子类。
  7. 性能异议先记下,重构完再调优。

8. 原文地图

主题原书章原文位置
示例程序与输出第1章 §1.1text/09-ch01.txt:35(搜「plays.json」)
先重构再加特性第1章 §1.2text/09-ch01.txt:140(搜「但发现代码因缺乏良好的结构」)
需求变化使重构必要第1章 §1.2text/09-ch01.txt:160(搜「是需求的变化使重构变得必要」)
测试先行箴言第1章 §1.3text/09-ch01.txt:180(搜「可靠的测试集」)
提炼 amountFor第1章 §1.4text/09-ch01.txt:243(搜「amountFor」)
小步箴言第1章 §1.4text/09-ch01.txt:325(搜「以微小的步伐修改程序」)
参数带类型命名第1章 §1.4text/09-ch01.txt:389(搜「动态类型语言」)
性能警钟与处理第1章 §1.4text/09-ch01.txt:559(搜「敲响警钟」) · text/09-ch01.txt:887(搜「大多数情况下可以忽略它」)
移除 volumeCredits 的 4 小步第1章 §1.4text/09-ch01.txt:890(搜「一共有4」)
保持数据不可变第1章 §1.6text/09-ch01.txt:1184(搜「尽量保持数据不可变」)
44 行到 70 行第1章 §1.7text/09-ch01.txt:1511(搜「44行增加到了70行」)
工厂函数的由来第1章 §1.8text/09-ch01.txt:1746(搜「JavaScript的构造函数里无法返回子类」)
三个节点第1章 §1.10text/09-ch01.txt:1961(搜「3个较为重要的节点」)
好代码的标准第1章 §1.10text/09-ch01.txt:1979(搜「轻而易举地修改它」) · text/09-ch01.txt:1984(搜「小的步子可以更快前进」)

Footnotes

  1. 出处:「重构,第一个示例」第 35 段(text/09-ch01.txt:35,搜「plays.json」);输出在第 117 段(text/09-ch01.txt:117,搜「Statement for BigCo」)。 2

  2. 出处:「重构,第一个示例」第 140 段(text/09-ch01.txt:140,搜「但发现代码因缺乏良好的结构」)。

  3. 出处:「重构,第一个示例」第 154 段(text/09-ch01.txt:154,搜「需求通常不来则已」)。

  4. 出处:「重构,第一个示例」第 160 段(text/09-ch01.txt:160,搜「是需求的变化使重构变得必要」)。

  5. 出处:「重构,第一个示例」第 238 段(text/09-ch01.txt:238,搜「转瞬即逝的灵光」)。

  6. 出处:「重构,第一个示例」第 325 段(text/09-ch01.txt:325,搜「以微小的步伐修改程序」)。

  7. 出处:「重构,第一个示例」第 389 段(text/09-ch01.txt:389,搜「动态类型语言」)。命名习惯引自 Kent Beck。

  8. 出处:「重构,第一个示例」第 887 段(text/09-ch01.txt:887,搜「大多数情况下可以忽略它」)。警钟一段在第 559 段(text/09-ch01.txt:559,搜「敲响警钟」)。

  9. 出处:「重构,第一个示例」第 1961 段(text/09-ch01.txt:1961,搜「3个较为重要的节点」)。

  10. 出处:「重构,第一个示例」第 1184 段(text/09-ch01.txt:1184,搜「尽量保持数据不可变」)。

  11. 出处:「重构,第一个示例」第 1511 段(text/09-ch01.txt:1511,搜「44行增加到了70行」)。

  12. 出处:「重构,第一个示例」第 1979 段(text/09-ch01.txt:1979,搜「轻而易举地修改它」)。

  13. 出处:「重构,第一个示例」第 1981 段(text/09-ch01.txt:1981,搜「重构的节奏感」)与第 1984 段(text/09-ch01.txt:1984,搜「小的步子可以更快前进」)。

  14. 出处:「重构,第一个示例」第 1517 段(text/09-ch01.txt:1517,搜「营地法则」)。

  15. 出处:「重构,第一个示例」第 19 段(text/09-ch01.txt:19,搜「大系统的一部分」)。原话:「我只能带你看看如何在一个我选择的小程序中进行重构,然而坦白说,那个程序的规模根本不值得我们这么做。但是,如果我给你看的代码是大系统的一部分,重构技术很快就变得重要起来。」

  16. 出处:「前言」第 145 段(text/07-fm.txt:145,搜「用JavaScript进行重构」)。JS 仅为示例语言的说法见第 128 段(text/07-fm.txt:128,搜「我选择了用JavaScript」)。