企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持知识库和私有化部署方案 广告
到目前为止,我的重构主要是为原函数添加足够的结构,以便我能更好地理解它,看清它的逻辑结构。这也是重构早期的一般步骤。把复杂的代码块分解为更小的单元,与好的命名一样都很重要。现在,我可以更多关注我要修改的功能部分了,也就是为这张详单提供一个HTML版本。不管怎么说,现在改起来更加简单了。因为计算代码已经被分离出来,我只需要为顶部的7行代码实现一个HTML的版本。问题是,这些分解出来的函数嵌套在打印文本详单的函数中。无论嵌套函数组织得多么良好,我总不想将它们全复制粘贴到另一个新函数中。我希望同样的计算函数可以被文本版详单和HTML版详单共用。 要实现复用有许多种方法,而我最喜欢的技术是拆分阶段(154)。这里我的目标是将逻辑分成两部分:一部分计算详单所需的数据,另一部分将数据渲染成文本或HTML。第一阶段会创建一个中转数据结构,再把它传递给第二阶段。 要开始拆分阶段(154),我会先对组成第二阶段的代码应用提炼函数(106)。在这个例子中,这部分代码就是打印详单的代码,其实也就是`statement`函数的全部内容。我要把它们与所有嵌套的函数一起抽取到一个新的顶层函数中,并将其命名为`renderPlainText`。 ``` function statement (invoice, plays) { return renderPlainText(invoice, plays); } function renderPlainText(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 totalAmount() {...} function totalVolumeCredits() {...} function usd(aNumber) {...} function volumeCreditsFor(aPerformance) {...} function playFor(aPerformance) {...} function amountFor(aPerformance) {...} ``` 编译、测试、提交,接着创建一个对象,作为在两个阶段间传递的中转数据结构,然后将它作为第一个参数传递给`renderPlainText`(然后编译、测试、提交)。 ``` function statement (invoice, plays) { const statementData = {}; return renderPlainText(statementData, invoice, plays); } function renderPlainText(data, 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 totalAmount() {...} function totalVolumeCredits() {...} function usd(aNumber) {...} function volumeCreditsFor(aPerformance) {...} function playFor(aPerformance) {...} function amountFor(aPerformance) {...} ``` 现在我要检查一下`renderPlainText`用到的其他参数。我希望将它们挪到这个中转数据结构里,这样所有计算代码都可以被挪到`statement`函数中,让`renderPlainText`只操作通过`data`参数传进来的数据。 第一步是将顾客(customer)字段添加到中转对象里(编译、测试、提交)。 ``` function statement (invoice, plays) { const statementData = {}; statementData.customer = invoice.customer; return renderPlainText(statementData, invoice, plays); } function renderPlainText(data, invoice, plays) { let result = `Statement for ${data.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; ``` 我将`performances`字段也搬移过去,这样我就可以移除掉`renderPlainText`的`invoice`参数(编译、测试、提交)。 ##### 顶层作用域... ``` function statement (invoice, plays) { const statementData = {}; statementData.customer = invoice.customer; statementData.performances = invoice.performances; return renderPlainText(statementData, invoice, plays); } function renderPlainText(data, plays) { let result = `Statement for ${data.customer}\n`; for (let perf of data.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 renderPlainText... ``` function totalAmount() { let result = 0; for (let perf of data.performances) { result += amountFor(perf); } return result; } function totalVolumeCredits() { let result = 0; for (let perf of data.performances) { result += volumeCreditsFor(perf); } return result; } ``` 现在,我希望“剧目名称”信息也从中转数据中获得。为此,需要使用`play`中的数据填充`aPerformance`对象(记得编译、测试、提交)。 ``` function statement (invoice, plays) { const statementData = {}; statementData.customer = invoice.customer; statementData.performances = invoice.performances.map(enrichPerformance); return renderPlainText(statementData, plays); function enrichPerformance(aPerformance) { const result = Object.assign({}, aPerformance); return result; } ``` 现在我只是简单地返回了一个`aPerformance`对象的副本,但马上我就会往这条记录中添加新的数据。返回副本的原因是,我不想修改传给函数的参数,我总是尽量保持数据不可变(immutable)——可变的状态会很快变成烫手的山芋。 > 在不熟悉 JavaScript 的人看来,`result = Object.assign({}, aPerformance)`的写法可能十分奇怪。它返回的是一个浅副本。虽然我更希望有个函数来完成此功能,但这个用法已经约定俗成,如果我自己写个函数,在JavaScript程序员看来反而会格格不入。 现在我们已经有了安放`play`字段的地方,可以把数据放进去。我需要对`playFor`和`statement`函数应用搬移函数(198)(然后编译、测试、提交)。 ##### function statement... ``` function enrichPerformance(aPerformance) { const result = Object.assign({}, aPerformance); result.play = playFor(result); return result; } function playFor(aPerformance) { return plays[aPerformance.playID]; } ``` 然后替换`renderPlainText`中对`playFor`的所有引用点,让它们使用新数据(编译、测试、提交)。 ##### function renderPlainText... ``` let result = `Statement for ${data.customer}\n`; for (let perf of data.performances) { result += ` ${perf.play.name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`; } result += `Amount owed is ${usd(totalAmount())}\n`; result += `You earned ${totalVolumeCredits()} credits\n`; return result; function volumeCreditsFor(aPerformance) { let result = 0; result += Math.max(aPerformance.audience - 30, 0); if ("comedy" === aPerformance.play.type) result += Math.floor(aPerformance.audience / 5); return result; } functionamountFor(aPerformance){ let result = 0; switch (aPerformance.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: ${aPerformance.play.type}`); } return result; } ``` 接着我使用类似的手法搬移`amountFor`函数(编译、测试、提交)。 ##### function statement... ``` function enrichPerformance(aPerformance) { const result = Object.assign({}, aPerformance); result.play = playFor(result); result.amount = amountFor(result); return result; } function amountFor(aPerformance) {...} ``` ##### function renderPlainText... ``` let result = `Statement for ${data.customer}\n`; for (let perf of data.performances) { result += ` ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`; } result += `Amount owed is ${usd(totalAmount())}\n`; result += `You earned ${totalVolumeCredits()} credits\n`; return result; function totalAmount() { let result = 0; for (let perf of data.performances) { result += perf.amount; } return result; } ``` 接下来搬移观众量积分的计算(编译、测试、提交)。 ##### function statement... ``` function enrichPerformance(aPerformance) { const result = Object.assign({}, aPerformance); result.play = playFor(result); result.amount = amountFor(result); result.volumeCredits = volumeCreditsFor(result); return result; } function volumeCreditsFor(aPerformance) {...} ``` ##### function renderPlainText... ``` function totalVolumeCredits() { let result = 0; for (let perf of data.performances) { result += perf.volumeCredits; } return result; } ``` 最后,我将两个计算总数的函数搬移到`statement`函数中。 ##### function statement... ``` const statementData = {}; statementData.customer = invoice.customer; statementData.performances = invoice.performances.map(enrichPerformance); statementData.totalAmount = totalAmount(statementData); statementData.totalVolumeCredits = totalVolumeCredits(statementData); return renderPlainText(statementData, plays); function totalAmount(data) {...} function totalVolumeCredits(data) {...} ``` ##### function renderPlainText... ``` let result = `Statement for ${data.customer}\n`; for (let perf of data.performances) { result += ` ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`; } result += `Amount owed is ${usd(data.totalAmount)}\n`; result += `You earned ${data.totalVolumeCredits} credits\n`; return result; ``` 尽管我可以修改函数体,让这些计算总数的函数直接使用`statementData`变量(反正它在作用域内),但我更喜欢显式地传入函数参数。 等到搬移完成,编译、测试、提交也做完,我便忍不住以管道取代循环(231)对几个地方进行重构。 ##### function renderPlainText... ``` function totalAmount(data) { return data.performances .reduce((total, p) => total + p.amount, 0); } function totalVolumeCredits(data) { return data.performances .reduce((total, p) => total + p.volumeCredits, 0); } ``` 现在我可以把第一阶段的代码提炼到一个独立的函数里了(编译、测试、提交)。 ##### 顶层作用域... ``` function statement (invoice, plays) { return renderPlainText(createStatementData(invoice, plays)); } function createStatementData(invoice, plays) { const statementData = {}; statementData.customer = invoice.customer; statementData.performances = invoice.performances.map(enrichPerformance); statementData.totalAmount = totalAmount(statementData); statementData.totalVolumeCredits = totalVolumeCredits(statementData); return statementData; ``` 由于两个阶段已经彻底分离,我干脆把它搬移到另一个文件里去(并且修改了返回结果的变量名,与我一贯的编码风格保持一致)。 ##### statement.js... `import createStatementData from './createStatementData.js';`##### createStatementData.js... ``` export default function createStatementData(invoice, plays) { const result = {}; result.customer = invoice.customer; result.performances = invoice.performances.map(enrichPerformance); result.totalAmount = totalAmount(result); result.totalVolumeCredits = totalVolumeCredits(result); return result; function enrichPerformance(aPerformance) {...} function playFor(aPerformance) {...} function amountFor(aPerformance) {...} function volumeCreditsFor(aPerformance) {...} function totalAmount(data) {...} function totalVolumeCredits(data) {...} ``` 最后再做一次编译、测试、提交,接下来,要编写一个HTML版本的对账单就很简单了。 ##### statement.js... ``` function htmlStatement (invoice, plays) { return renderHtml(createStatementData(invoice, plays)); } function renderHtml (data) { let result = `<h1>Statement for ${data.customer}</h1>\n`; result += "<table>\n"; result += "<tr><th>play</th><th>seats</th><th>cost</th></tr>"; for (let perf of data.performances) { result += ` <tr><td>${perf.play.name}</td><td>${perf.audience}</td>`; result += `<td>${usd(perf.amount)}</td></tr>\n`; } result += "</table>\n"; result += `<p>Amount owed is <em>${usd(data.totalAmount)}</em></p>\n`; result += `<p>You earned <em>${data.totalVolumeCredits}</em> credits</p>\n`; return result; } function usd(aNumber) {...} ``` (我把`usd`函数也搬移到顶层作用域中,以便`renderHtml`也能访问它。)