在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。
设计不是在一开始完成的,而是在整个开发过程中逐渐浮现出来。
需求的变化使得重构变得必要。
有一个戏剧演出团,演员们经常要去各种场合表演戏剧。
通常客户(customer)会指定几出剧目,而剧团则根据观众(audience)人数及剧目类型来向客户收费。
戏剧种类:悲剧(tragedy)和喜剧(comedy)。
给客户账单时,剧团还会根据到场观众的数量给出“观众量积分”(volume credit)优惠。
剧目的数据plays.json:
{
"hamlet": {"name": "Hamlet", "type": "tragedy"},
"as-like": {"name": "As You Like It", "type": "comedy"},
"othello": {"name": "Othello", "type": "tragedy"}
}
账单数据invoices.json:
[
{
"customer": "BigCo",
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "as-like",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
]
一个打印账单详情的功能(1.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
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}`);
}
// 计算积分
volumeCredits += Math.max(perf.audience - 30, 0);
if ("comedy" === play.type) {
volumeCredits += Math.floor(perf.audience / 5);
}
// 添加待打印的字符串
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;
}
当我亲手打完这个例子,惊!我发现这不就是我写代码的方式嘛!!!
不管代码多简单,别人看都需要花一点功夫才能看懂你在做什么。
好了,代码就是这样了,如果没什么其他需求,那这些代码就这么放着也行,然鹅万恶的需求发生了变化。。。
result+=xxx
,如果写的代码时间一长,万一不小心就漏了哪里呢,汗(代码五分钟,bug两小时)。如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后在添加该特性。
相信大家刚开始写代码的时候肯定是写几行就编译一下,看看编译器有没有error,不然等你写了几十行,然后几十个error、warning。。。
尽管有错误提示,新手可能根本看不出来对应的错误是怎么产生的,头疼。
现在我们已经不是新手了,已经是写了很多bug的程序员了。在重构代码的时候,你改了半天,然后run。。。惊!最后程序运行的结果不一样了,只能调试一下,或者无法调试,最后找了半天,只能git恢复以前的版本。(亲身体验)
根据以往的经验,我们需要一套运行时间短的测试集。当我们改了一部分我们就需要验证其正确性。
在理解一段代码的时候,我总是先看一些变量和函数的命名,大概推测出要做什么,然后具体细节就要仔细看看代码逻辑。
而这一段代码显然全是代码逻辑细节,所以我们可以把这些细节抽取出一个个函数。
首先提取出中间的switch(2.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
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); // 待支付的金额
// 计算积分
volumeCredits += Math.max(perf.audience - 30, 0);
if ("comedy" === play.type) {
volumeCredits += Math.floor(perf.audience / 5);
}
// 添加待打印的字符串
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;
/**
* 计算一场表演的金额
*
* @param perf 一场表演
* @param plays 所有的剧目
*/
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;
}
}
做完修改立刻测试并且保存到本地的git。
函数提取出来后看看是不是要修改一下变量名。
作者的编程风格:永远将函数的返回值命名为"result",这样一眼就能看出它的作用。(3.html)
/**
* 计算一场表演的金额
*
* @param perf 一场表演
* @param plays 所有的剧目
*/
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;
}
做完修改立刻测试并且保存到本地的git。
修改函数的参数名。
作者的编程风格:使用一门动态类型语言,跟踪变量的类型,为参数取名时都默认带上其类型名。(4.html)
/**
* 计算一场表演的金额
*
* @param aPerformance 一场表演
* @param plays 所有的剧目
*/
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;
}
做完修改立刻测试并且保存到本地的git。
还是观察函数的参数,发现aPerformance是每个循环都会改变的,play可以根据aPerformance得到。
所以可以移除一个参数。这里使用以查询代替临时变量。(5.hmtl)
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
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); // 待支付的金额
// 计算积分
volumeCredits += Math.max(perf.audience - 30, 0);
if ("comedy" === play.type) {
volumeCredits += Math.floor(perf.audience / 5);
}
// 添加待打印的字符串
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;
/**
* 得到一场表演的剧目
*
* @param aPerformance 一场表演
* @return 剧目
*/
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
}
做完修改立刻测试并且保存到本地的git。
使用内联变量 去掉play(6.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
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)); // 待支付的金额
// 计算积分
volumeCredits += Math.max(perf.audience - 30, 0);
if ("comedy" === playFor(perf).type) {
volumeCredits += Math.floor(perf.audience / 5);
}
// 添加待打印的字符串
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;
}
做完修改立刻测试并且保存到本地的git。
下面就要移除play参数了,首先修改函数内部(7.html):
/**
* 计算一场表演的金额
*
* @param aPerformance 一场表演
* @param plays 所有的剧目
*/
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;
}
做完修改立刻测试并且保存到本地的git。
修改函数参数和调用(8.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
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); // 待支付的金额
// 计算积分
volumeCredits += Math.max(perf.audience - 30, 0);
if ("comedy" === playFor(perf).type) {
volumeCredits += Math.floor(perf.audience / 5);
}
// 添加待打印的字符串
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;
/**
* 计算一场表演的金额
*
* @param aPerformance 一场表演
*/
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;
}
}
做完修改立刻测试并且保存到本地的git。
再看函数调用处,发现赋值给thisAmount后就不再改变,继续使用内联变量(9.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
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 += Math.max(perf.audience - 30, 0);
if ("comedy" === playFor(perf).type) {
volumeCredits += Math.floor(perf.audience / 5);
}
// 添加待打印的字符串
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;
}
做完修改立刻测试并且保存到本地的git。
再看看statement函数里面还有一段观众量积分的计算逻辑,提取出来(10.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
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);
// 添加待打印的字符串
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;
/**
* 计算观众量积分
*
* @param aPerformance 一场表演
*/
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;
}
}
做完修改立刻测试并且保存到本地的git。
修改函数变量名(参数名、返回值名)(11.html):
/**
* 计算观众量积分
*
* @param aPerformance 一场表演
*/
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;
}
做完修改立刻测试并且保存到本地的git。
看看现在的statement,还是要移除临时变量,发现format没有变化过,换成函数(12.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
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);
// 添加待打印的字符串
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;
/**
* 格式化货币数字
*
* @param aNumber 货币数字
* @return 格式化后的货币数字
*/
function format(aNumber) {
return new Intl.NumberFormat("en-US", {style: "currency", currency: "USD", minimumFractionDigits: 2}).format(aNumber);
}
}
做完修改立刻测试并且保存到本地的git。
感觉format这名字虽然在特定情况下清楚是干什么的,但是表意还不是特别明确,就修改函数声明(有了好的函数声明,就不必阅读代码体理解其行为了,这里是就用货币的类型USD)(13.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
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);
// 添加待打印的字符串
result += ` ${playFor(perf).name}: ${usd(amountFor(perf)/100)} (${perf.audience} seats)\n`;
// 更新总金额
totalAmount += amountFor(perf);
}
// 添加待打印的字符串
result += `Amount owed is ${usd(totalAmount/100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
/**
* 格式化货币数字
*
* @param aNumber 货币数字
* @return 格式化后的货币数字
*/
function usd(aNumber) {
return new Intl.NumberFormat("en-US", {style: "currency", currency: "USD", minimumFractionDigits: 2}).format(aNumber);
}
}
做完修改立刻测试并且保存到本地的git。
再来看看还有哪些临时变量,volumeCredits和totalAmount都是通过循环修改的,所以我们发现一个循环其实干了多件事情,使用拆分循环(14.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
function statement(invoice, plays) {
let totalAmount = 0; // 待支付的总金额
let result = `Statement for ${invoice.customer}\n`; // 生成的待打印的字符串
// 遍历账单所有的剧目
for (let perf of invoice.performances) {
// 添加待打印的字符串
result += ` ${playFor(perf).name}: ${usd(amountFor(perf)/100)} (${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/100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
}
做完修改立刻测试并且保存到本地的git。
提炼函数(15.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
function statement(invoice, plays) {
let totalAmount = 0; // 待支付的总金额
let result = `Statement for ${invoice.customer}\n`; // 生成的待打印的字符串
// 遍历账单所有的剧目
for (let perf of invoice.performances) {
// 添加待打印的字符串
result += ` ${playFor(perf).name}: ${usd(amountFor(perf)/100)} (${perf.audience} seats)\n`;
// 更新总金额
totalAmount += amountFor(perf);
}
let volumeCredits = totalVolumeCredits();
// 添加待打印的字符串
result += `Amount owed is ${usd(totalAmount/100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
/**
* 计算观众量积分
*
* @return 观众量积分
*/
function totalVolumeCredits() {
let result = 0;
for (let perf of invoice.performances) {
result += volumeCreditsFor(perf);
}
return result;
}
}
做完修改立刻测试并且保存到本地的git。
内联变量(16.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
*/
function statement(invoice, plays) {
let totalAmount = 0; // 待支付的总金额
let result = `Statement for ${invoice.customer}\n`; // 生成的待打印的字符串
// 遍历账单所有的剧目
for (let perf of invoice.performances) {
// 添加待打印的字符串
result += ` ${playFor(perf).name}: ${usd(amountFor(perf)/100)} (${perf.audience} seats)\n`;
// 更新总金额
totalAmount += amountFor(perf);
}
// 添加待打印的字符串
result += `Amount owed is ${usd(totalAmount/100)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
}
做完修改立刻测试并且保存到本地的git。
用同样的步骤移除totalAmount(17.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function statement(invoice, plays) {
let result = `Statement for ${invoice.customer}\n`;
// 遍历账单所有的剧目
for (let perf of invoice.performances) {
result += ` ${playFor(perf).name}: ${usd(amountFor(perf)/100)} (${perf.audience} seats)\n`;
}
result += `Amount owed is ${usd(totalAmount()/100)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
/**
* 计算金额
*
* @return 金额
*/
function totalAmount() {
let result = 0;
for (let perf of invoice.performances) {
result += amountFor(perf);
}
return result;
}
}
做完修改立刻测试并且保存到本地的git。
我们到现在为止已经把计算相关的逻辑从主函数中移除,变成了一组函数,就理解上来说函数已经到了一目了然的程度,阔怕。
目前我们已经可以看清楚代码的结构了,现在可以思考一下关于html格式输出的功能了,貌似只要修改一下statement里面的几个文本就行了,呵呵,CV开始了。
作为一个想提高代码复用的程序员,我还是希望不把一个函数的代码全部复制粘贴到另一个函数的。
所以我们需要拆分阶段(第一阶段:计算数据 第二阶段:渲染文本),第一个阶段会创建一个中转数据结构给第二阶段,提炼第二阶段函数(18.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function statement(invoice, plays) {
return renderPlainTest(invoice, plays);
}
/**
* 渲染打印的账单
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function renderPlainTest(invoice, plays) {
let result = `Statement for ${invoice.customer}\n`;
// 遍历账单所有的剧目
for (let perf of invoice.performances) {
result += ` ${playFor(perf).name}: ${usd(amountFor(perf)/100)} (${perf.audience} seats)\n`;
}
result += `Amount owed is ${usd(totalAmount()/100)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
}
做完修改立刻测试并且保存到本地的git。
添加中转数据结构(19.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function statement (invoice, plays) {
const statementData = {};
return renderPlainTest(statementData, invoice, plays);
}
/**
* 渲染打印的账单
*
* @param data 中转数据结构
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function renderPlainTest(data, invoice, plays) {
let result = `Statement for ${invoice.customer}\n`;
// 遍历账单所有的剧目
for (let perf of invoice.performances) {
result += ` ${playFor(perf).name}: ${usd(amountFor(perf)/100)} (${perf.audience} seats)\n`;
}
result += `Amount owed is ${usd(totalAmount()/100)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
}
做完修改立刻测试并且保存到本地的git。
检测一下renderPlainTest用到的其他参数,可以放到data里面,先移动customer和performances(20.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function statement(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances;
return renderPlainTest(statementData, invoice, plays);
}
/**
* 渲染打印的账单
*
* @param data 中转数据结构
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function renderPlainTest(data, invoice, plays) {
let result = `Statement for ${data.customer}\n`;
// 遍历账单所有的剧目
for (let perf of data.performances) {
result += ` ${playFor(perf).name}: ${usd(amountFor(perf)/100)} (${perf.audience} seats)\n`;
}
result += `Amount owed is ${usd(totalAmount()/100)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
/**
* 计算金额
*
* @return 金额
*/
function totalAmount() {
let result = 0;
for (let perf of data.performances) {
result += amountFor(perf);
}
return result;
}
/**
* 计算观众量积分
*
* @return 观众量积分
*/
function totalVolumeCredits() {
let result = 0;
for (let perf of data.performances) {
result += volumeCreditsFor(perf);
}
return result;
}
}
做完修改立刻测试并且保存到本地的git。
接下里我们希望剧目信息也从data中获取(21.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function statement(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
return renderPlainTest(statementData, invoice, plays);
/**
* map映射函数
*/
function enrichPerformance(aPerformance) {
const result = Object.assign({}, aPerformance); // 浅拷贝
result.play = playFor(result);
return result;
/**
* 得到一场表演的剧目
*
* @param aPerformance 一场表演
* @return 剧目
*/
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
}
}
做完修改立刻测试并且保存到本地的git。
替换renderPlainTest中的playFor引用(22.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function statement(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
return renderPlainTest(statementData, plays);
}
/**
* 渲染打印的账单
*
* @param data 中转数据结构
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function renderPlainTest(data, plays) {
let result = `Statement for ${data.customer}\n`;
// 遍历账单所有的剧目
for (let perf of data.performances) {
result += ` ${perf.play.name}: ${usd(amountFor(perf)/100)} (${perf.audience} seats)\n`;
}
result += `Amount owed is ${usd(totalAmount()/100)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
/**
* 计算观众量积分
*
* @param aPerformance 一场表演
*/
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;
}
/**
* 计算一场表演的金额
*
* @param aPerformance 一场表演
*/
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;
}
}
做完修改立刻测试并且保存到本地的git。
替换renderPlainTest中的amountFor引用(23.html):
/**
* map映射函数
*/
function enrichPerformance(aPerformance) {
const result = Object.assign({}, aPerformance); // 浅拷贝
result.play = playFor(result);
result.amount = amountFor(result);
return result;
/**
* 计算一场表演的金额
*
* @param aPerformance 一场表演
*/
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;
}
}
/**
* 渲染打印的账单
*
* @param data 中转数据结构
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function renderPlainTest(data, plays) {
let result = `Statement for ${data.customer}\n`;
// 遍历账单所有的剧目
for (let perf of data.performances) {
result += ` ${perf.play.name}: ${usd(perf.amount/100)} (${perf.audience} seats)\n`;
}
result += `Amount owed is ${usd(totalAmount()/100)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
/**
* 计算金额
*
* @return 金额
*/
function totalAmount() {
let result = 0;
for (let perf of data.performances) {
result += perf.amount;
}
return result;
}
}
做完修改立刻测试并且保存到本地的git。
搬移renderPlainTest中的观众量积分(24.html):
/**
* 计算观众量积分
*
* @return 观众量积分
*/
function totalVolumeCredits() {
let result = 0;
for (let perf of data.performances) {
result += perf.volumeCredits;
}
return result;
}
/**
* map映射函数
*/
function enrichPerformance(aPerformance) {
const result = Object.assign({}, aPerformance); // 浅拷贝
result.play = playFor(result);
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
/**
* 计算观众量积分
*
* @param aPerformance 一场表演
*/
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;
}
}
做完修改立刻测试并且保存到本地的git。
将两个计算总数的函数搬到statement函数中(25.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function statement(invoice, plays) {
const statementData = {};
statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
return renderPlainTest(statementData, plays);
/**
* 计算金额
*
* @return 金额
*/
function totalAmount() {
let result = 0;
for (let perf of statementData.performances) {
result += perf.amount;
}
return result;
}
/**
* 计算观众量积分
*
* @return 观众量积分
*/
function totalVolumeCredits() {
let result = 0;
for (let perf of statementData.performances) {
result += perf.volumeCredits;
}
return result;
}
}
/**
* 渲染打印的账单
*
* @param data 中转数据结构
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function renderPlainTest(data, plays) {
let result = `Statement for ${data.customer}\n`;
// 遍历账单所有的剧目
for (let perf of data.performances) {
result += ` ${perf.play.name}: ${usd(perf.amount/100)} (${perf.audience} seats)\n`;
}
result += `Amount owed is ${usd(data.totalAmount/100)}\n`;
result += `You earned ${data.totalVolumeCredits} credits\n`;
return result;
}
做完修改立刻测试并且保存到本地的git。
以管道代替循环(26.html):
/**
* 计算金额
*
* @return 金额
*/
function totalAmount() {
return statementData.performances.reduce((total, p) => total + p.amount, 0);
}
/**
* 计算观众量积分
*
* @return 观众量积分
*/
function totalVolumeCredits() {
return statementData.performances.reduce((total, p) => total + p.volumeCredits, 0);
}
做完修改立刻测试并且保存到本地的git。
提炼第一阶段的代码(27.html):
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function statement(invoice, plays) {
return renderPlainTest(createStatementData(invoice, plays));
}
/**
* 计算账单数据
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
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;
}
做完修改立刻测试并且保存到本地的git。
两个阶段分离到两个文件 并且添加html的功能(28):
28.html:
<html>
<head>
<meta charset="utf-8">
<title>测试title>
head>
<body>
<script type="module" src="./statement.js">script>
<script type="module">
import htmlStatement from './statement.js';
let invoice = [
{
"customer": "BigCo",
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "as-like",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
];
let plays = {
"hamlet": {"name": "Hamlet", "type": "tragedy"},
"as-like": {"name": "As You Like It", "type": "comedy"},
"othello": {"name": "Othello", "type": "tragedy"}
};
let body = document.getElementsByTagName("body")[0];
body.innerHTML = htmlStatement(invoice[0], plays);
script>
body>
html>
statement.js:
import createStatementData from './createStatementData.js';
/**
* 打印账单详情
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
function statement(invoice, plays) {
return renderPlainTest(createStatementData(invoice, plays));
}
/**
* 渲染打印的账单
*
* @param data 中转数据结构
* @return 待打印的字符串
*/
function renderPlainTest(data) {
let result = `Statement for ${data.customer}\n`;
for (let perf of data.performances) {
result += ` ${perf.play.name}: ${usd(perf.amount/100)} (${perf.audience} seats)\n`;
}
result += `Amount owed is ${usd(data.totalAmount/100)}\n`;
result += `You earned ${data.totalVolumeCredits} credits\n`;
return result;
}
/**
* 打印账单详情(HTML版)
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的HTML
*/
export default function htmlStatement(invoice, plays) {
return renderHtml(createStatementData(invoice, plays));
}
/**
* 渲染打印的账单(HTML版)
*
* @param data 中转数据结构
* @return 待打印的HTML
*/
function renderHtml(data) {
let result = `Statement for
${data.customer}\n`;
result += "\n";
result += "play seats cost ";
for (let perf of data.performances) {
result += ` ${perf.play.name} ${perf.audience} \n`;
result += `${usd(perf.amount/100)} \n`;
}
result += "
\n";
result += `Amount owed is
${usd(data.totalAmount/100)}\n`;
result += `You earned
${data.totalVolumeCredits} credits\n`;
return result;
}
/**
* 格式化货币数字
*
* @param aNumber 货币数字
* @return 格式化后的货币数字
*/
function usd(aNumber) {
return new Intl.NumberFormat("en-US", {style: "currency", currency: "USD", minimumFractionDigits: 2}).format(aNumber);
}
createStatementData.js:
/**
* 计算账单数据
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
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;
/**
* map映射函数
*/
function enrichPerformance(aPerformance) {
const result = Object.assign({}, aPerformance); // 浅拷贝
result.play = playFor(result);
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
/**
* 得到一场表演的剧目
*
* @param aPerformance 一场表演
* @return 剧目
*/
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
/**
* 计算一场表演的金额
*
* @param aPerformance 一场表演
*/
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;
}
/**
* 计算观众量积分
*
* @param aPerformance 一场表演
*/
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;
}
}
/**
* 计算观众量积分
*
* @return 观众量积分
*/
function totalVolumeCredits(data) {
return result.performances.reduce((total, p) => total + p.volumeCredits, 0);
}
/**
* 计算金额
*
* @return 金额
*/
function totalAmount(data) {
return result.performances.reduce((total, p) => total + p.amount, 0);
}
}
运行结果:
做完修改立刻测试并且保存到本地的git。
我们添加html版本的功能已经很容易了,无需重复计算部分的逻辑。
我们终于完成了第一个需求,下面要解决演员的努力问题了。。。
支持更多类型的戏剧,我们需要amountFor添加switch的分支,这种分支很容易随着代码堆积而腐坏。
解决方法有许多,这里我们使用类型多态来解决这个问题(毕竟这个早就接触过了)。
我们需要建立一个继承体系,目前有两个子类:喜剧和悲剧,子类包含各自的计算逻辑,使用以多态取代条件表达式。
再看我们现在的代码,我们可以直接忽略关于格式化的代码,只要不改变中转的数据结构就行了。
我们给新的类们起个名字叫计数器。(29)
createStatementData.js:
/**
* 计算器基类
*/
class PerformanceCalculator {
constructor(aPerformance, aPlay) {
this.performance = aPerformance;
this.play = aPlay;
}
}
/**
* map映射函数
*/
function enrichPerformance(aPerformance) {
const calculator = new PerformanceCalculator(aPerformance, playFor(aPerformance));
const result = Object.assign({}, aPerformance); // 浅拷贝
result.play = calculator.play;
result.amount = amountFor(result);
result.volumeCredits = volumeCreditsFor(result);
return result;
}
做完修改立刻测试并且保存到本地的git。
将函数搬移进计数器(30):
createStatementData.js:
/**
* 计算器基类
*/
class PerformanceCalculator {
constructor(aPerformance, aPlay) {
this.performance = aPerformance;
this.play = aPlay;
}
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;
}
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;
}
}
/**
* map映射函数
*/
function enrichPerformance(aPerformance) {
const calculator = new PerformanceCalculator(aPerformance, playFor(aPerformance));
const result = Object.assign({}, aPerformance); // 浅拷贝
result.play = calculator.play;
result.amount = calculator.amount;
result.volumeCredits = calculator.volumeCredits;
return result;
}
做完修改立刻测试并且保存到本地的git。
让计算器表现出多态性,以子类取代类型码(31):
createStatementData.js:
/**
* 计算器基类
*/
class PerformanceCalculator {
constructor(aPerformance, aPlay) {
this.performance = aPerformance;
this.play = aPlay;
}
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);
}
}
/**
* 计算器工厂
*/
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}');
}
}
/**
* 计算账单数据
*
* @param invoice 账单
* @param plays 所有的剧目
* @return 待打印的字符串
*/
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;
/**
* map映射函数
*/
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;
/**
* 得到一场表演的剧目
*
* @param aPerformance 一场表演
* @return 剧目
*/
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
}
/**
* 计算观众量积分
*
* @return 观众量积分
*/
function totalVolumeCredits(data) {
return result.performances.reduce((total, p) => total + p.volumeCredits, 0);
}
/**
* 计算金额
*
* @return 金额
*/
function totalAmount(data) {
return result.performances.reduce((total, p) => total + p.amount, 0);
}
}
做完修改立刻测试并且保存到本地的git。
我们终于把代码改成适于添加新剧种的结构,不同剧种的运算逻辑全都集中到了一处。添加一个新剧种就只要添加一个新的子类,并且在工厂中返回它。
什么算好的代码,作者提倡的标准就是人们是否能轻而易举地修改它。
为什么程序如此难以相与? | 设计与重构的目标 |
---|---|
难以阅读的程序,难以修改 | 容易阅读 |
逻辑重复的程序,难以修改 | 所有逻辑都只在唯一地点指定 |
添加新行为时需要修改已有代码的程序,难以修改 | 新的改动不会危及现有行为 |
带复杂条件逻辑的程序,难以修改 | 尽可能简单表达条件逻辑 |
我觉得这一章的内容非常重要,识别出代码的坏味道,是正确重构的前提。
重构记录的格式:名称 速写 动机 做法 范例
后面还没看。。。
完整代码:https://pan.baidu.com/s/11AVSyjZcCgwby8BTxM_4uw
提取码:0nil