ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
每当看到这样长长的函数,我便下意识地想从整个函数中分离出不同的关注点。第一个引起我注意的就是中间那段`switch`语句。 ```js function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format; for (let perf of invoice.performances) { const play = plays[perf.playID]; let thisAmount = 0; switch (play.type) { case "tragedy": thisAmount = 40000; if (perf.audience > 30) { thisAmount += 1000 * (perf.audience - 30); } break; case "comedy": thisAmount = 30000; if (perf.audience > 20) { thisAmount += 10000 + 500 * (perf.audience - 20); } thisAmount += 300 * perf.audience; break; default: throw new Error(`unknown type: ${play.type}`); } // add volume credits volumeCredits += Math.max(perf.audience - 30, 0); // add extra credit for every ten comedy attendees if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5); // print line for this order result += ` ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`; totalAmount += thisAmount; } result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; } ``` 看着这块代码,我就知道它在计算一场戏剧演出的费用。这是我的直觉。不过正如Ward Cunningham所说,这种理解只是我脑海中转瞬即逝的灵光。我需要梳理这些灵感,将它们从脑海中搬回到代码里去,以免忘记。这样当我回头看时,代码就能告诉我它在干什么,我不需要重新思考一遍。 要将我的理解转化到代码里,得先将这块代码抽取成一个独立的函数,按它所干的事情给它命名,比如叫`amountFor(performance)`。每次想将一块代码抽取成一个函数时,我都会遵循一个标准流程,最大程度减少犯错的可能。我把这个流程记录了下来,并将它命名为提炼函数(106),以便日后可以方便地引用。 首先,我需要检查一下,如果我将这块代码提炼到自己的一个函数里,有哪些变量会离开原本的作用域。在此示例中,是`perf`、`play`和`thisAmount`这3个变量。前两个变量会被提炼后的函数使用,但不会被修改,那么我就可以将它们以参数方式传递进来。我更关心那些会被修改的变量。这里只有唯一一个——`thisAmount`,因此可以将它从函数中直接返回。我还可以将其初始化放到提炼后的函数里。修改后的代码如下所示。 ##### function statement... ``` function amountFor(perf, play) { let thisAmount = 0; switch (play.type) { case "tragedy": thisAmount = 40000; if (perf.audience > 30) { thisAmount += 1000 * (perf.audience - 30); } break; case "comedy": thisAmount = 30000; if (perf.audience > 20) { thisAmount += 10000 + 500 * (perf.audience - 20); } thisAmount += 300 * perf.audience; break; default: throw new Error(`unknown type: ${play.type}`); } return thisAmount; } ``` 当我在代码块上方使用了斜体(中文对应为楷体)标记的题头“ *function xxx* ”时,表明该代码块位于题头所在函数、文件或类的作用域内。通常该作用域内还有其他的代码,但由于不是讨论重点,因此把它们隐去不展示。 现在原`statement`函数可以直接调用这个新函数来初始化`thisAmount`。 ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format; for (let perf of invoice.performances) { const play = plays[perf.playID]; let thisAmount = amountFor(perf, play); // add volume credits volumeCredits += Math.max(perf.audience - 30, 0); // add extra credit for every ten comedy attendees if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5); // print line for this order result += ` ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`; totalAmount += thisAmount; } result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` 做完这个改动后,我会马上编译并执行一遍测试,看看有无破坏了其他东西。无论每次重构多么简单,养成重构后即运行测试的习惯非常重要。犯错误是很容易的——至少我知道我是很容易犯错的。做完一次修改就运行测试,这样在我真的犯了错时,只需要考虑一个很小的改动范围,这使得查错与修复问题易如反掌。这就是重构过程的精髓所在:小步修改,每次修改后就运行测试。如果我改动了太多东西,犯错时就可能陷入麻烦的调试,并为此耗费大把时间。小步修改,以及它带来的频繁反馈,正是防止混乱的关键。 > 这里我使用的“编译”一词,指的是将JavaScript变为可执行代码之前的所有步骤。虽然JavaScript可以直接执行,有时可能不需任何步骤,但有时可能需要将代码移动到一个输出目录,或使用Babel这样的代码处理器等。 因为是JavaScript,我可以直接将`amountFor`提炼成为`statement`的一个内嵌函数。这个特性十分有用,因为我就不需要再把外部作用域中的数据传给新提炼的函数。这个示例中可能区别不大,但也是少了一件要操心的事。 > ![](https://box.kancloud.cn/9cf522e33e311401bf0d755d003df8ea_19x20.jpeg) 重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。 做完上面的修改,测试是通过的,因此下一步我要把代码提交到本地的版本控制系统。我会使用诸如git或mercurial这样的版本控制系统,因为它们可以支持本地提交。每次成功的重构后我都会提交代码,如果待会不小心搞砸了,我便能轻松回滚到上一个可工作的状态。把代码推送(push)到远端仓库前,我会把零碎的修改压缩成一个更有意义的提交(commit)。 提炼函数(106)是一个常见的可自动完成的重构。如果我是用Java编程,我会本能地使用IDE的快捷键来完成这项重构。在我撰写本书时,JavaScript工具对此重构的支持仍不是很健壮,因此我必须手动重构。这不是很难,当然我还是需要小心处理那些局部作用域的变量。 完成提炼函数(106)手法后,我会看看提炼出来的函数,看是否能进一步提升其表达能力。一般我做的第一件事就是给一些变量改名,使它们更简洁,比如将`thisAmount`重命名为`result`。 ##### function statement... ``` function amountFor(perf, play) { let result = 0; switch (play.type) { case "tragedy": result = 40000; if (perf.audience > 30) { result += 1000 * (perf.audience - 30); } break; case "comedy": result = 30000; if (perf.audience > 20) { result += 10000 + 500 * (perf.audience - 20); } result += 300 * perf.audience; break; default: throw new Error(`unknown type: ${play.type}`); } return result; } ``` 这是我个人的编码风格:永远将函数的返回值命名为“result”,这样我一眼就能知道它的作用。然后我再次编译、测试、提交代码。接着,我前往下一个目标——函数参数。 ##### function statement... ``` function amountFor(aPerformance, play) { let result = 0; switch (play.type) { case "tragedy": result = 40000; if (aPerformance.audience > 30) { result += 1000 * (aPerformance.audience - 30); } break; case "comedy": result = 30000; if (aPerformance.audience > 20) { result += 10000 + 500 * (aPerformance.audience - 20); } result += 300 * aPerformance.audience; break; default: throw new Error(`unknown type: ${play.type}`); } return result; } ``` 这是我的另一个编码风格。使用一门动态类型语言(如JavaScript)时,跟踪变量的类型很有意义。因此,我为参数取名时都默认带上其类型名。一般我会使用不定冠词修饰它,除非命名中另有解释其角色的相关信息。这个习惯是从Kent Beck那里学的\[Beck SBPP\],到现在我还一直觉得很有用。 > ![](https://box.kancloud.cn/9cf522e33e311401bf0d755d003df8ea_19x20.jpeg) 傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。 这次改名是否值得我大费周章呢?当然值得。好代码应能清楚地表明它在做什么,而变量命名是代码清晰的关键。只要改名能够提升代码的可读性,那就应该毫不犹豫去做。有好的查找替换工具在手,改名通常并不困难;此外,你的测试以及语言本身的静态类型支持,都可以帮你揪出漏改的地方。如今有了自动化的重构工具,即便要给一个被大量调用的函数改名,通常也不在话下。 本来下一个要改名的变量是`play`,但我对这个参数另有安排。 ### 移除play变量 观察`amountFor`函数时,我会看看它的参数都从哪里来。`aPerformance`是从循环变量中来,所以自然每次循环都会改变,但`play`变量是由`performance`变量计算得到的,因此根本没必要将它作为参数传入,我可以在`amountFor`函数中重新计算得到它。当我分解一个长函数时,我喜欢将`play`这样的变量移除掉,因为它们创建了很多具有局部作用域的临时变量,这会使提炼函数更加复杂。这里我要使用的重构手法是以查询取代临时变量(178)。 我先从赋值表达式的右边部分提炼出一个函数来。 ##### function statement... ``` function playFor(aPerformance) { return plays[aPerformance.playID]; } ``` ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format; for (let perf of invoice.performances) { const play = playFor(perf); let thisAmount = amountFor(perf, play); // add volume credits volumeCredits += Math.max(perf.audience - 30, 0); // add extra credit for every ten comedy attendees if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5); // print line for this order result += ` ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`; totalAmount += thisAmount; } result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` 编译、测试、提交,然后使用内联变量(123)手法内联`play`变量。 ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format; for (let perf of invoice.performances) { const play = playFor(perf); let thisAmount = amountFor(perf, playFor(perf)); // add volume credits volumeCredits += Math.max(perf.audience - 30, 0); // add extra credit for every ten comedy attendees if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5); // print line for this order result += ` ${playFor(perf).name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`; totalAmount += thisAmount; } result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` 编译、测试、提交。完成变量内联后,我可以对`amountFor`函数应用改变函数声明(124),移除`play`参数。我会分两步走。首先在`amountFor`函数内部使用新提炼的函数。 ##### function statement... ``` function amountFor(aPerformance, play) { let result = 0; switch (playFor(aPerformance).type) { case "tragedy": result = 40000; if (aPerformance.audience > 30) { result += 1000 * (aPerformance.audience - 30); } break; case "comedy": result = 30000; if (aPerformance.audience > 20) { result += 10000 + 500 * (aPerformance.audience - 20); } result += 300 * aPerformance.audience; break; default: throw new Error(`unknown type: ${playFor(aPerformance).type}`); } return result; } ``` 编译、测试、提交,最后将参数删除。 ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format; for (let perf of invoice.performances) { let thisAmount = amountFor(perf , playFor(perf) ); // add volume credits volumeCredits += Math.max(perf.audience - 30, 0); // add extra credit for every ten comedy attendees if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5); // print line for this order result += ` ${playFor(perf).name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`; totalAmount += thisAmount; } result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` ##### function statement... ``` function amountFor(aPerformance , play ) { let result = 0; switch (playFor(aPerformance).type) { case "tragedy": result = 40000; if (aPerformance.audience > 30) { result += 1000 * (aPerformance.audience - 30); } break; case "comedy": result = 30000; if (aPerformance.audience > 20) { result += 10000 + 500 * (aPerformance.audience - 20); } result += 300 * aPerformance.audience; break; default: throw new Error(`unknown type: ${playFor(aPerformance).type}`); } return result; } ``` 然后再一次编译、测试、提交。 这次重构可能在一些程序员心中敲响警钟:重构前,查找`play`变量的代码在每次循环中只执行了1次,而重构后却执行了3次。我会在后面探讨重构与性能之间的关系,但现在,我认为这次改动还不太可能对性能有严重影响,即便真的有所影响,后续再对一段结构良好的代码进行性能调优,也容易得多。 移除局部变量的好处就是做提炼时会简单得多,因为需要操心的局部作用域变少了。实际上,在做任何提炼前,我一般都会先移除局部变量。 处理完`amountFor`的参数后,我回过头来看一下它的调用点。它被赋值给一个临时变量,之后就不再被修改,因此我又采用内联变量(123)手法内联它。 ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format; for (let perf of invoice.performances) { // add volume credits volumeCredits += Math.max(perf.audience - 30, 0); // add extra credit for every ten comedy attendees if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5); // print line for this order result += ` ${playFor(perf).name}: ${format(amountFor(perf)/100)} (${perf.audience} seats)\n`; totalAmount += amountFor(perf); } result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` ### 提炼计算观众量积分的逻辑 现在`statement`函数的内部实现是这样的。 ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format; for (let perf of invoice.performances) { // add volume credits volumeCredits += Math.max(perf.audience - 30, 0); // add extra credit for every ten comedy attendees if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5); // print line for this order result += ` ${playFor(perf).name}: ${format(amountFor(perf)/100)} (${perf.audience} seats)\n`; totalAmount += amountFor(perf); } result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` 这会儿我们就看到了移除`play`变量的好处,移除了一个局部作用域的变量,提炼观众量积分的计算逻辑又更简单一些。 我仍需要处理其他两个局部变量。`perf`同样可以轻易作为参数传入,但`volumeCredits`变量则有些棘手。它是一个累加变量,循环的每次迭代都会更新它的值。因此最简单的方式是,将整块逻辑提炼到新函数中,然后在新函数中直接返回`volumeCredits`。 ##### function statement... ``` function volumeCreditsFor(perf) { let volumeCredits = 0; volumeCredits += Math.max(perf.audience - 30, 0); if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5); return volumeCredits; } ``` ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format; for (let perf of invoice.performances) { volumeCredits += volumeCreditsFor(perf); // print line for this order result += ` ${playFor(perf).name}: ${format(amountFor(perf)/100)} (${perf.audience} seats)\n`; totalAmount += amountFor(perf); } result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` 我还顺便删除了多余(并且会引起误解)的注释。 编译、测试、提交,然后对新函数里的变量改名。 ##### function statement... ``` function volumeCreditsFor(aPerformance) { let result = 0; result += Math.max(aPerformance.audience - 30, 0); if ("comedy" === playFor(aPerformance).type) result += Math.floor(aPerformance.audience / 5); return result; } ``` 这里我只展示了一步到位的改名结果,不过实际操作时,我还是一次只将一个变量改名,并在每次改名后执行编译、测试、提交。 ### 移除format变量 我们再看一下`statement`这个主函数。 ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; const format = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format; for (let perf of invoice.performances) { volumeCredits += volumeCreditsFor(perf); // print line for this order result += ` ${playFor(perf).name}: ${format(amountFor(perf)/100)} (${perf.audience} seats)\n`; totalAmount += amountFor(perf); } result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` 正如我上面所指出的,临时变量往往会带来麻烦。它们只在对其进行处理的代码块中有用,因此临时变量实质上是鼓励你写长而复杂的函数。因此,下一步我要替换掉一些临时变量,而最简单的莫过于从`format`变量入手。这是典型的“将函数赋值给临时变量”的场景,我更愿意将其替换为一个明确声明的函数。 ##### function statement... ``` function format(aNumber) { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format(aNumber); } ``` ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; for (let perf of invoice.performances) { volumeCredits += volumeCreditsFor(perf); // print line for this order result += ` ${playFor(perf).name}: ${format(amountFor(perf)/100)} (${perf.audience} seats)\n`; totalAmount += amountFor(perf); } result += `Amount owed is ${format(totalAmount/100)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` > 尽管将函数变量改变成函数声明也是一种重构手法,但我既未为此手法命名,也未将它纳入重构名录。还有很多的重构手法我都觉得没那么重要。我觉得上面这个函数改名的手法既十分简单又不太常用,不值得在重构名录中占有一席之地。 我对提炼得到的函数名称不很满意——`format`未能清晰地描述其作用。`formatAsUSD`很表意,但又太长,特别它仅是小范围地被用在一个字符串模板中。我认为这里真正需要强调的是,它格式化的是一个货币数字,因此我选取了一个能体现此意图的命名,并应用了改变函数声明(124)手法。 ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; for (let perf of invoice.performances) { volumeCredits += volumeCreditsFor(perf); // print line for this order result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`; totalAmount += amountFor(perf); } result += `Amount owed is ${usd(totalAmount)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` ##### function statement... ``` function usd(aNumber) { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2 }).format(aNumber/100); } ``` 好的命名十分重要,但往往并非唾手可得。只有恰如其分地命名,才能彰显出将大函数分解成小函数的价值。有了好的名称,我就不必通过阅读函数体来了解其行为。但要一次把名取好并不容易,因此我会使用当下能想到最好的那个。如果稍后想到更好的,我就会毫不犹豫地换掉它。通常你需要花几秒钟通读更多代码,才能发现最好的名称是什么。 重命名的同时,我还将重复的除以100的行为也搬移到函数里。将钱以美分为单位作为正整数存储是一种常见的做法,可以避免使用浮点数来存储货币的小数部分,同时又不影响用数学运算符操作它。不过,对于这样一个以美分为单位的整数,我又需要以美元为单位进行展示,因此让格式化函数来处理整除的事宜再好不过。 ### 移除观众量积分总和 我的下一个重构目标是`volumeCredits`。处理这个变量更加微妙,因为它是在循环的迭代过程中累加得到的。第一步,就是应用拆分循环(227)将`volumeCredits`的累加过程分离出来。 ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let volumeCredits = 0; let result = `Statement for ${invoice.customer}\n`; for (let perf of invoice.performances) { // print line for this order result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`; totalAmount += amountFor(perf); } for (let perf of invoice.performances) { volumeCredits += volumeCreditsFor(perf); } result += `Amount owed is ${usd(totalAmount)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` 完成这一步,我就可以使用移动语句(223)手法将变量声明挪动到紧邻循环的位置。 ##### top level… ``` function statement (invoice, plays) { let totalAmount = 0; let result = `Statement for ${invoice.customer}\n`; for (let perf of invoice.performances) { // print line for this order result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`; totalAmount += amountFor(perf); } let volumeCredits = 0; for (let perf of invoice.performances) { volumeCredits += volumeCreditsFor(perf); } result += `Amount owed is ${usd(totalAmount)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` 把与更新`volumeCredits`变量相关的代码都集中到一起,有利于以查询取代临时变量(178)手法的施展。第一步同样是先对变量的计算过程应用提炼函数(106)手法。 ##### function statement... ``` function totalVolumeCredits() { let volumeCredits = 0; for (let perf of invoice.performances) { volumeCredits += volumeCreditsFor(perf); } return volumeCredits; } ``` ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let result = `Statement for ${invoice.customer}\n`; for (let perf of invoice.performances) { // print line for this order result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`; totalAmount += amountFor(perf); } let volumeCredits = totalVolumeCredits(); result += `Amount owed is ${usd(totalAmount)}\n`; result += `You earned ${volumeCredits} credits\n`; return result; ``` 完成函数提炼后,我再应用内联变量(123)手法内联`totalVolumeCredits`函数。 ##### 顶层作用域... ``` function statement (invoice, plays) { let totalAmount = 0; let result = `Statement for ${invoice.customer}\n`; for (let perf of invoice.performances) { // print line for this order result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`; totalAmount += amountFor(perf); } result += `Amount owed is ${usd(totalAmount)}\n`; result += `You earned ${totalVolumeCredits()} credits\n`; return result; ``` 重构至此,让我先暂停一下,谈谈刚刚完成的修改。首先,我知道有些读者会再次对此修改可能带来的性能问题感到担忧,我知道很多人本能地警惕重复的循环。但大多数时候,重复一次这样的循环对性能的影响都可忽略不计。如果你在重构前后进行计时,很可能甚至都注意不到运行速度的变化——通常也确实没什么变化。许多程序员对代码实际的运行路径都所知不足,甚至经验丰富的程序员有时也未能避免。在聪明的编译器、现代的缓存技术面前,我们很多直觉都是不准确的。软件的性能通常只与代码的一小部分相关,改变其他的部分往往对总体性能贡献甚微。 当然,“大多数时候”不等同于“所有时候”。有时,一些重构手法也会显著地影响性能。但即便如此,我通常也不去管它,继续重构,因为有了一份结构良好的代码,回头调优其性能也容易得多。如果我在重构时引入了明显的性能损耗,我后面会花时间进行性能调优。进行调优时,可能会回退我早先做的一些重构——但更多时候,因为重构我可以使用更高效的调优方案。最后我得到的是既整洁又高效的代码。 因此对于重构过程的性能问题,我总体的建议是:大多数情况下可以忽略它。如果重构引入了性能损耗,先完成重构,再做性能优化。 其次,我希望你能注意到:我们移除`volumeCredits`的过程是多么小步。整个过程一共有4步,每一步都伴随着一次编译、测试以及向本地代码库的提交: - 使用拆分循环(227)分离出累加过程; - 使用移动语句(223)将累加变量的声明与累加过程集中到一起; - 使用提炼函数(106)提炼出计算总数的函数; - 使用内联变量(123)完全移除中间变量。 我得坦白,我并非总是如此小步——但在事情变复杂时,我的第一反应就是采用更小的步子。怎样算变复杂呢,就是当重构过程有测试失败而我又无法马上看清问题所在并立即修复时,我就会回滚到最后一次可工作的提交,然后以更小的步子重做。这得益于我如此频繁地提交。特别是与复杂代码打交道时,细小的步子是快速前进的关键。 接着我要重复同样的步骤来移除`totalAmount`。我以拆解循环开始(编译、测试、提交),然后下移累加变量的声明语句(编译、测试、提交),最后再提炼函数。这里令我有点头疼的是:最好的函数名应该是`totalAmount`,但它已经被变量名占用,我无法起两个同样的名字。因此,我在提炼函数时先给它随便取了一个名字(然后编译、测试、提交)。 ##### function statement... ``` function appleSauce() { let totalAmount = 0; for (let perf of invoice.performances) { totalAmount += amountFor(perf); } return totalAmount; } ``` ##### 顶层作用域... ``` function statement (invoice, plays) { let result = `Statement for ${invoice.customer}\n`; for (let perf of invoice.performances) { result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`; } let totalAmount = appleSauce(); result += `Amount owed is ${usd(totalAmount)}\n`; result += `You earned ${totalVolumeCredits()} credits\n`; return result; ``` 接着我将变量内联(编译、测试、提交),然后将函数名改回`totalAmount`(编译、测试、提交)。 ##### 顶层作用域... ``` function statement (invoice, plays) { let result = `Statement for ${invoice.customer}\n`; for (let perf of invoice.performances) { result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`; } result += `Amount owed is ${usd(totalAmount())}\n`; result += `You earned ${totalVolumeCredits()} credits\n`; return result; ``` ##### function statement... ``` function totalAmount() { let totalAmount = 0; for (let perf of invoice.performances) { totalAmount += amountFor(perf); } return totalAmount; } ``` 趁着给新提炼的函数改名的机会,我顺手一并修改了函数内部的变量名,以便保持我一贯的编码风格。 ##### function statement... ``` function totalAmount() { let result = 0; for (let perf of invoice.performances) { result += amountFor(perf); } return result; } function totalVolumeCredits() { let result = 0; for (let perf of invoice.performances) { result += volumeCreditsFor(perf); } return result; } ```