重构,改善既有代码的设计读后感-至大量嵌套函数
该书的第一章遥进行重构的代码(补充了一些中文注释):
//剧目数据
var plays =
{
"hamlet":{"name":"Hamlet","type":"tragedy"},
"as-like":{"name":"As You Like It","type":"comedy"},
"othello":{"name":"othello","type":"tragedy"}
}
//账单
var invoices =
[
{
"customer":"BigCo", //客户
"performances":[ //演出情况
{
"payID":"hamlet",//莎士比亚的哈姆雷特
"audience":55 //观众数
},
{
"payID":"as-like",//莎士比亚的皆大欢喜
"audience":35
},
{
"payID":"othello",//莎士比亚的奥赛罗
"audience":40
}
]
}
]
function statement ( invoice,plays ) {
let totalAmount = 0 ; // 账单总额
let volumeCredits = 0 ; //观众量积分,用于获取折扣,提升客户忠诚度
let result = `Statement for ${invoice.customer}\n`; //用于打印的字符串
//用于格式化数字,优化显示货币。
//locale 是必传参数(支持不同国家的数字分隔符格式)
//option 是可选参数是一个对象
//style,currency 可以设置货币符号或者精确度计算
//unit 是可选参数,可以增加单位(长度单位等)
const format = new Intl.NumberFormat("en-US",{ style:"currency",currency:"USD",minimumFractionDigits:2}).format;
for(let perf of invoice.performances){
const play = plays[perf.payID];
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); //取两者较大数,防止结果小于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;
}
通过调用函数statement ( invoices[0],plays )
便可以打印出结果
如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于修改,那就先重构那个程序使其比较容易添加该特性,然后再添加该特性。
重构前,先检查自己是否有一套可靠的测试集。这些测试,必须有自我检验能力。
本书第一章举了一个例子,将一个函数进行拆分重构。以我不太充分的知识库看来,主要做的就是:
1.将函数中独立的功能重新写成函数,在顶层函数调用。
2.对顶层函数与拆分出来的函数进行变量名优化。
3.将一些临时变量提炼成为函数,通过直接调用函数来获取。
4.将一些在循环中累加的变量,将其剥离出来,在函数中重新遍历累加,以实现以查询取代临时变量的目的。
有两个问题:
1.提炼临时变量,会造成多次调用函数
2.同一个循环拆成多个同样会造成性能损耗
书中写道:
大多数时候,重复一次这样的循环对性能的影响都可以忽略不计。
当然,’大多数时候‘不等同于‘所有时候’。有时,一些重构手法会显著的影响性能。但即便如此,我也不去管他,继续重构,因为有了一份结构良好的代码,回头调优其性能也会容易得多。如果我在重构时引入了明显的性能消耗,我后面会花时间进行性能调优。进行调优时,可能会回退我早先做的一些重构----但更多时候,因为重构,我可以使用更高效的调优方案。最后我得到的是既整洁又高效的代码。
但是针对实际的工作环境,在特定时间长度下,能做的工作有限。所以个人觉得,并非所有重构都要按部就班,可以根据实际情况具体分析。如果时间不够,对于一些可能会影响性能的暂时忽略。如果时间允许,则可以尝试,先重构,后调优的路子。---仅个人见解
编程时,需要遵循营地法则:保证你离开时的代码库一定比来时更健康。
实际编程中,普通程序员实现可能比较难。因为在一定的时间内,除了要完成任务之外还要重构会耽误文本的开发计划。而且重构并不能保证代码不会出现bug。程序很脆弱,每一次改动都应当有比较完备的测试。
实际上,可以针对一些方法进行简单重构,过程也不一定时持续的,也不一定是一个人。
更好的时有领导决定,以任务的形式派发下来,给重构代码的程序员时间。
重构技术,就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现他。
实际工作中,可能不需要这样,如果仅仅是 如修改参数变量名这样的事情。当然了,如果步伐更好,出错概率也更小,好处是显而易见的。这是在测试容易的情况下。如果测试过程比较耗时,就需要你自己平衡关系了。
跟随书中示例代码,大概的见识一下重构的过程。
第一眼比较容易的看到的时swich语句,这段代码用来计算每一种剧目需要的花费,所以可以考虑将这段抽取成独立的函数。这个过程称为 提炼函数(106).
抽取的代码 涉及三个临时变量,其中perf,play仅仅是使用,不会修改,可以作为参数传过来,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 aPerformance of invoice.performances){
const play = plays[aPerformance.payID];
let thisAmount = amountFor(aPerformance,play) ;
//计算观众积分 add volume credits
volumeCredits += Math.max(aPerformance.audience - 30,0); //取两者较大数,防止结果小于0
//add extra credit for every ten comedy attendees
if("comedy" === play.type) volumeCredits += Math.floor(aPerformance.audience / 5);
//print line for this order
result += ` ${play.name}:${format(thisAmount/100)} (${aPerformance.audience} seats)\n`;
totalAmount += thisAmount;
}
result += `Amount owed is ${format(totalAmount/100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
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;
}
}
最上层函数看起来明晰了很多。
以查询取代临时变量(178)
观察提炼出来的amountFor函数的参数,可以发现第一个参数是从循环中取得,每次都是变化的,但是第二个参数是由performances计算得来,因此没必要将其作为参数引入。
//内联变量
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 aPerformance of invoice.performances){
let thisAmount = amountFor(aPerformance,playFor(aPerformance)) ;
//计算观众积分 add volume credits
volumeCredits += Math.max(aPerformance.audience - 30,0); //取两者较大数,防止结果小于0
//add extra credit for every ten comedy attendees
if("comedy" === playFor(aPerformance).type) volumeCredits += Math.floor(aPerformance.audience / 5);
//print line for this order
result += ` ${playFor(aPerformance).name}:${format(thisAmount/100)} (${aPerformance.audience} seats)\n`;
totalAmount += thisAmount;
}
result += `Amount owed is ${format(totalAmount/100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
function playFor(aperformance) {
return plays[aperformance.playID];
}
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;
}
}
改变函数声明(124)
//改变函数声明(124)
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 aPerformance of invoice.performances){
let thisAmount = amountFor(aPerformance) ;
//计算观众积分 add volume credits
volumeCredits += Math.max(aPerformance.audience - 30,0); //取两者较大数,防止结果小于0
//add extra credit for every ten comedy attendees
if("comedy" === playFor(aPerformance).type) volumeCredits += Math.floor(aPerformance.audience / 5);
//print line for this order
result += ` ${playFor(aPerformance).name}:${format(thisAmount/100)} (${aPerformance.audience} seats)\n`;
totalAmount += thisAmount;
}
result += `Amount owed is ${format(totalAmount/100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
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;
}
}
提炼计算观众积分的逻辑
将函数变量改成函数声明,同时注意函数名要尽量表意。
//改变函数声明(124)
function statement ( invoice,plays ) {
let totalAmount = 0 ; // 账单总额
let volumeCredits = 0 ; //观众量积分,用于获取折扣,提升客户忠诚度
let result = `Statement for ${invoice.customer}\n`; //用于打印的字符串
for(let aPerformance of invoice.performances){
let thisAmount = amountFor(aPerformance) ;
//计算观众积分 add volume credits
volumeCredits += volumeCreditsFor(aPerformance);
//print line for this order
result += ` ${playFor(aPerformance).name}:${usd(thisAmount/100)} (${aPerformance.audience} seats)\n`;
totalAmount += thisAmount;
}
result += `Amount owed is ${usd(totalAmount/100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
//这一轮循环增加的量
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance - 30 , 0);
if ("comedy" == playFor(aPerformance).type) result += Math.floor(aPerformance.audience / 5);
return result;
}
function usd(aNumber) {
return new Intl.NumberFormat("en-US",
{ style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber/100);
}
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
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;
}
}
移除观众量总积分
//移除观众量积分总和
function statement ( invoice,plays ) {
let totalAmount = 0 ; // 账单总额
let result = `Statement for ${invoice.customer}\n`; //用于打印的字符串
for(let aPerformance of invoice.performances){
//print line for this order
result += ` ${playFor(aPerformance).name}:${usd(amountFor(aPerformance)/100)} (${aPerformance.audience} seats)\n`;
totalAmount += amountFor(aPerformance);
}
let volumeCredits = totalVolumeCredits();//计算观众积分 add volume credits
result += `Amount owed is ${usd(totalAmount/100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
//计算观众积分 add volume credits
function totalVolumeCredits() {
let volumeCredits = 0 ; //观众量积分,用于获取折扣,提升客户忠诚度
for(let aPerformance of invoice.performances){
//计算观众积分 add volume credits
volumeCredits += volumeCreditsFor(aPerformance);
}
return volumeCredits;
}
//这一轮循环增加的量
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance - 30 , 0);
if ("comedy" == playFor(aPerformance).type) result += Math.floor(aPerformance.audience / 5);
return result;
}
function usd(aNumber) {
return new Intl.NumberFormat("en-US",
{ style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber/100);
}
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
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;
}
}
其中使用了
拆分循环(227) 分离出累加过程
移动语句(223) 将累加变量的声明与累加过程集中到一起
提炼函数(106) 提炼出计算总和的函数
内联变量(123) 完全移除中间变量
同样的步骤来移除totalAmount
//移除观众量积分总和
function statement ( invoice,plays ) {
let result = `Statement for ${invoice.customer}\n`; //用于打印的字符串
for(let aPerformance of invoice.performances){
//print line for this order
result += ` ${playFor(aPerformance).name}:${usd(amountFor(aPerformance)/100)} (${aPerformance.audience} seats)\n`;
}
result += `Amount owed is ${usd(totalAmount()/100)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
//计算总的账目
function totalAmount() {
let totalAmount = 0 ; // 账单总额
for(let aPerformance of invoice.performances){
totalAmount += amountFor(aPerformance);
}
return totalAmount;
}
//计算观众积分 add volume credits
function totalVolumeCredits() {
let volumeCredits = 0 ; //观众量积分,用于获取折扣,提升客户忠诚度
for(let aPerformance of invoice.performances){
//计算观众积分 add volume credits
volumeCredits += volumeCreditsFor(aPerformance);
}
return volumeCredits;
}
//这一轮循环增加的量
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance - 30 , 0);
if ("comedy" == playFor(aPerformance).type) result += Math.floor(aPerformance.audience / 5);
return result;
}
//格式化数字,显示为货币
function usd(aNumber) {
return new Intl.NumberFormat("en-US",
{ style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber/100);
}
//获取某一剧目
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
//计算某一剧目需要的账目
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;
}
}
将函数折叠后再看,是不是思路比较清晰明了了
daima.png