重构 新版 - A First Example
重构 新版 - A First Example
- THE STARTING POINT
- DECOMPOSING THE STATEMENT FUNCTION
- Removing the play Variable
- Extracting Volume Credits
- Removing the format Variable
- Removing Total Volume Credits
- STATUS: LOTS OF NESTED FUNCTIONS
- SPLITTING THE PHASES OF CALCULATION AND FORMATTING
- REORGANIZING THE CALCULATIONS BY TYPE
- Creating a Performance Calculator
- Moving Functions into the Calculator
- Making the Performance Calculator Polymorphic
- STATUS: CREATING THE DATA WITH THE POLYMORPHIC CALCULATOR
THE STARTING POINT
一家戏剧公司,演员会参加表演。戏剧分为喜剧和悲剧。公司根据观众的规模和戏剧的类型收费。除了账单,还有volume credits,未来的折扣。
plays.json的数据是这样的:
{
"hamlet": {"name": "Hamlet", "type": "tragedy"},
"aslike": {"name": "As You Like It", "type": "comedy"},
"othello": {"name": "Othello", "type": "tragedy"}
}
账单,invoices.json文件是这样的:
[
{
"customer": "BigCo",
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "aslike",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
]
打印账单的函数:
function statement (invoice, plays) {
let totalAmount = 0;
let volumeCredits = 0;
let result = `Statement for ${invoice.customer}\n`;
const format = new Intl.NumberFormat("enUS",
{ 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);
totalAmount += thisAmount;
}
result += `Amount owed is ${format(totalAmount/100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
}
输出是这样的:
Statement for BigCo
Hamlet: $650.00 (55 seats)
As You Like It: $580.00 (35 seats)
Othello: $500.00 (40 seats)
Amount owed is $1,730.00
You earned 47 credits
DECOMPOSING THE STATEMENT FUNCTION
先从switch开始重构(Extract Function),其中,把thisAmount重命名为result,把perf重命名为aPerformance。
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;
}
这样,statement修改为:
let thisAmount = amountFor(perf, play);
Removing the play Variable
play可以在amountFor内重新计算。
重构长函数的时候,喜欢摆脱像play这样的变量。因为临时变量会创建很多本地范围内的名称,这些名称使得提取变得负责。这叫Replace Temp with Query。
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
statement修改成:
const play = playFor(perf);
然后,去掉play变量:
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);
totalAmount += thisAmount;
}
然后可以Change Function Declaration,删除play参数:
function amountFor(aPerformance) {
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;
}
我们再看amountFor,它成了一个不再修改的临时变量。于是我们应用Inline Variable。
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);
totalAmount += amountFor(perf);
}
Extracting Volume Credits
删除play变量以后,通过删除一个本地范围的变量,就容易抽取volume credits计算了。
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;
}
修改statement:
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);
totalAmount += amountFor(perf);
}
Removing the format Variable
function format(aNumber) {
return new Intl.NumberFormat("enUS",
{ 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);
totalAmount += amountFor(perf);
}
result += `Amount owed is ${format(totalAmount/100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
format没有表达它正在做的事情,formatAsUSD又太长了,所以改成usd。这叫Change Function Declaration。
Removing Total Volume Credits
下一个目标是volumeCredits。它比较棘手,因为它是在循环期间生成的。可以使用Split Loop分开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);
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;
使用Slide Statements,把声明挪到下一个循环。
let volumeCredits = 0;
for (let perf of invoice.performances) {
volumeCredits += volumeCreditsFor(perf);
}
然后,我们抽取函数:
function totalVolumeCredits() {
let volumeCredits = 0;
for (let perf of invoice.performances) {
volumeCredits += volumeCreditsFor(perf);
}
return volumeCredits;
}
statement修改为:
result += `Amount owed is ${usd(totalAmount)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
然后,删除totalAmount:
function totalAmount() {
let result = 0;
for (let perf of invoice.performances) {
result += amountFor(perf);
}
return result;
}
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);
}
result += `Amount owed is ${usd(totalAmount())}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
STATUS: LOTS OF NESTED FUNCTIONS
现在,我们看一下完整的代码:
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);
}
result += `Amount owed is ${usd(totalAmount())}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
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;
}
function usd(aNumber) {
return new Intl.NumberFormat("enUS",
{ style: "currency", currency: "USD",
minimumFractionDigits: 2 }).format(aNumber/100);
}
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;
}
function amountFor(aPerformance) {
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;
}
}
现在,代码结构好多了。*的statement函数只有几行代码,用来打印。计算逻辑都被移到支持函数。
SPLITTING THE PHASES OF CALCULATION AND FORMATTING
下来,聚焦功能改进-提供HTML版本的statement。
使用Split Phase技术。先把业务逻辑分成两部分,一是计算,二是渲染。
先使用Extract Function,实现Split Phase。
function statement (invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances;
return renderPlainText(statementData, 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);
}
result += `Amount owed is ${usd(totalAmount())}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
其中,statementData对象里包含customer和performances。相应地,也要修改totalAmount()和totalVolumeCredits()函数。
现在,希望play name来自中间数据-使用play来丰富performance。
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
return renderPlainText(statementData, plays);
function enrichPerformance(aPerformance) {
//浅拷贝
const result = Object.assign({}, aPerformance);
return result;
}
然后,Move Function:
function enrichPerformance(aPerformance) {
const result = Object.assign({}, aPerformance);
result.play = playFor(result);
return result;
}
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
这样,renderPlainText函数的playFor(perf).name可以修改为perf.play.name。
同样修改volumeCreditsFor(aPerformance)和amountFor(aPerformance)函数。
再使用同样的办法移动amountFor和volumeCreditsFor。
function enrichPerformance(aPerformance) {
const result = Object.assign({}, aPerformance);
result.play = playFor(result);
result.amountFor = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
这样,statement函数成了:
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;
这样,可以保存成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) {
const result = Object.assign({}, aPerformance);
result.play = playFor(result);result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
function amountFor(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;
}
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;
}
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);
}
很容易写HTML版本了:
import createStatementData from './createStatementData.js';
function statement (invoice, plays) {
return renderPlainText(createStatementData(invoice, plays));
}
function renderPlainText(data, plays) {
let result = `Statement for ${data.customer}\n`;
for (let perf of data.performances) {
result += ` ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)
}
result += `Amount owed is ${usd(data.totalAmount)}\n`;
result += `You earned ${data.totalVolumeCredits} credits\n`;
return result;
}
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) {
return new Intl.NumberFormat("enUS",
{ style: "currency", currency: "USD",
minimumFractionDigits: 2 }).format(aNumber/100);
}
REORGANIZING THE CALCULATIONS BY TYPE
要支持更多的play类别,每个都有自己的计费和volume credits计算。
对应的策略是Replace Conditional with Polymorphism。
Creating a Performance Calculator
enrichPerformance是关键。
function enrichPerformance(aPerformance) {
const calculator = new PerformanceCalculator(playFor(aPerformance));
const result = Object.assign({}, aPerformance);
result.play = calculator.play;
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
class PerformanceCalculator {
constructor(aPerformance, aPlay) {
this.performance = aPerformance;
this.play = aPlay;
}
}
Moving Functions into the Calculator
Move Function,把逻辑拷贝到新的上下文。
把aPerformance修改成this.performance,playFor(aPerformance)修改成this.play。
get amount() {
let result = 0;
switch (this.play.type) {
case "tragedy":
result = 40000;
if (this.performance.audience > 30) {
result += 1000 * (this.performance.audience 30);
}
break;
case "comedy":
result = 30000;
if (this.performance.audience > 20) {
result += 10000 + 500 * (this.performance.audience 20);
}
result += 300 * this.performance.audience;
break;
default:
throw new Error(`unknown type: ${this.play.type}`);
}
return result;
}
function amountFor(aPerformance) {
return new PerformanceCalculator(aPerformance, playFor(aPerformance)).amount;
}
再使用Inline Function:
result.amount = calculator.amount;
同理:
get volumeCredits() {
let result = 0;
result += Math.max(this.performance.audience 30, 0);
if ("comedy" === this.play.type)
result += Math.floor(this.performance.audience / 5);
return result;
}
result.volumeCredits = calculator.volumeCredits;
Making the Performance Calculator Polymorphic
首先Replace Type Code with Subclasses。
先Replace Constructor with Factory:
const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance));
function createPerformanceCalculator(aPerformance, aPlay) {
switch(aPlay.type) {
case "tragedy": return new TragedyCalculator(aPerformance, aPlay);
case "comedy" : return new ComedyCalculator(aPerformance, aPlay);
default:throw new Error(`unknown type: ${aPlay.type}`);
}
}
class TragedyCalculator extends PerformanceCalculator {
}
class ComedyCalculator extends PerformanceCalculator {
}
对于TragedyCalculator:
get amount() {
let result = 40000;
if (this.performance.audience > 30) {
result += 1000 * (this.performance.audience 30);
}
return result;
}
对于ComedyCalculator:
get amount() {
let result = 30000;
if (this.performance.audience > 20) {
result += 10000 + 500 * (this.performance.audience 20);
}
result += 300 * this.performance.audience;
return result;
}
于是,对于PerformanceCalculator:
get amount() {
throw new Error('subclass responsibility');
}
下一个条件替换是volume credits计算。
大多数plays要检查观众超过30,修改PerformanceCalculator:
get volumeCredits() {
return Math.max(this.performance.audience 30, 0);
}
对于ComedyCalculator:
get volumeCredits() {
return super.volumeCredits + Math.floor(this.performance.audience / 5);
}
STATUS: CREATING THE DATA WITH THE POLYMORPHIC CALCULATOR
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) {
const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance));
const result = Object.assign({}, aPerformance);
result.play = calculator.play;
result.amount = calculator.amount;
result.volumeCredits = calculator.volumeCredits;
return result;
}
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
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 createPerformanceCalculator(aPerformance, aPlay) {
switch(aPlay.type) {
case "tragedy": return new TragedyCalculator(aPerformance, aPlay);
case "comedy" : return new ComedyCalculator(aPerformance, aPlay);
default:
throw new Error(`unknown type: ${aPlay.type}`);
}
}
class PerformanceCalculator {
constructor(aPerformance, aPlay) {
this.performance = aPerformance;
this.play = aPlay;
}
get amount() {
throw new Error('subclass responsibility');
}
get volumeCredits() {
return Math.max(this.performance.audience 30, 0);
}
}
class TragedyCalculator extends PerformanceCalculator {
get amount() {
let result = 40000;
if (this.performance.audience > 30) {
result += 1000 * (this.performance.audience 30);
}
return result;
}
}
class ComedyCalculator extends PerformanceCalculator {
get amount() {
let result = 30000;
if (this.performance.audience > 20) {
result += 10000 + 500 * (this.performance.audience 20);
}
result += 300 * this.performance.audience;
return result;
}
get volumeCredits() {
return super.volumeCredits + Math.floor(this.performance.audience / 5);
}
}
上一篇: 文件读写和拷贝