重构 改善既有代码的设计 第二版 读书笔记

前言

重构的定义:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。
设计不是在一开始完成的,而是在整个开发过程中逐渐浮现出来。
需求的变化使得重构变得必要

第一章 重构 第一个示例

背景

  • 商家:戏剧演出团
  • 戏剧种类:悲剧(tragedy)和喜剧(comedy)
  • 客户(customer):指定几出剧目
  • 收费参考:观众(audience)人数、剧目类型
  • 优惠:根据到场观众的数量给出“观众量积分”(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;
}

运行结果:
在这里插入图片描述

当我亲手打完这个例子,惊!这不就是我写代码的方式嘛!!!
不管代码做的事情有多简单,别人看都需要花一点功夫才能看懂你在做什么。

好了,代码就是这样了,如果没什么其他需求,那这些代码就这么放着也行,然鹅没有什么意外的话,就要出意外了,万恶的需求发生了变化。。。

  1. 希望以HTML的格式输出账单
    我们需要修改每一个 result+=xxx ,如果写的代码时间一长,万一不小心就漏了哪里呢,汗(代码五分钟,bug两小时)。
  2. 演员通过不懈努力,学会了历史剧、田园剧等等等,剧目的种类增加
    我们需要添加switch的case,修改计费方式、积分计算方式。

重构例子

如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后在添加该特性。

1、可靠的测试集

相信大家刚开始写代码的时候肯定是写几行就编译一下,看看编译器有没有error,不然等你写了几十行,然后几十个error、warning。。。
尽管有错误提示,新手可能根本看不出来对应的错误是怎么产生的,头疼。

现在我们已经不是新手了,已经是写了很多bug的程序员了。在重构代码的时候,你改了半天,然后run。。。惊!最后程序运行的结果不一样了,只能调试一下,或者无法调试,最后找了半天,只能git恢复以前的版本。(亲身体验)

根据以往的经验,我们需要一套运行时间短的测试集。当我们改了一部分我们就需要验证其正确性。
在这个例子中,测试要做的事情就是当我们输入参数不变的时候,打印出来的结果也不变,也就是做字符串比较的操作。(这边书中没提供,我也没写)

2、分解函数statement

在理解一段代码的时候,我总是先看一些变量和函数的命名,大概推测出要做什么,然后具体细节就要仔细看看代码逻辑。
而这一段代码显然全是代码逻辑细节,所以我们可以把这些细节抽取出一个个函数。

2.1

首先提取出中间的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。

2.2

函数提取出来后看看是不是要修改一下变量名。
作者的编程风格:永远将函数的返回值命名为"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。

2.3

修改函数的参数名。
作者的编程风格:使用一门动态类型语言,跟踪变量的类型,为参数取名时都默认带上其类型名。(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。

2.4

还是观察函数的参数,发现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。

2.5

再看看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。

2.6

看看现在的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。

2.7

再来看看还有哪些临时变量,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。

2.8

用同样的步骤移除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。

我们到现在为止已经把计算相关的逻辑从主函数中移除,变成了一组函数,就理解上来说函数已经到了一目了然的程度,阔怕。

3、拆分计算阶段与格式化阶段

目前我们已经可以看清楚代码的结构了,现在可以思考一下关于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:

<!DOCTYPE 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 = `<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>\n`;
		result += `<td>${usd(perf.amount/100)}</td><tr>\n`;
	}
	
	result += "</table>\n";
	result += `<p>Amount owed is <em>${usd(data.totalAmount/100)}</em></p>\n`;
	result += `<p>You earned <em>${data.totalVolumeCredits}</em> credits</p>\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版本的功能已经很容易了,无需重复计算部分的逻辑。

4、按类型重组计算过程

我们终于完成了第一个需求,下面要解决演员的努力问题了。。。
支持更多类型的戏剧,我们需要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。

我们终于把代码改成适于添加新剧种的结构,不同剧种的运算逻辑全都集中到了一处。添加一个新剧种就只要添加一个新的子类,并且在工厂中返回它。

什么算好的代码,作者提倡的标准就是人们是否能轻而易举地修改它。

第二章 重构的原则

  1. 重构的定义
    • (名词形式)对软件内部结构的一种调整,目的是在不改变软件可察行为的前提下,提高可理解性,降低修改成本。
    • (动词形式)使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。
  2. 软件开发的两顶帽子
    • 添加新功能时,不应该修改既有代码,只管添加新功能并通过测试。(做到这个太难了)
    • 重构时不再添加新功能,只管改进程序结构,并通过已有测试。
  3. 为何重构
    • 重构改进软件设计(Design)消除重复代码,我就可以确定所有事物和行为在代码中只表述一次。
    • 重构使软件更容易理解(Maintain)好让以后接手的人看得懂
    • 重构帮助找到BUG(Debug)顺着计算机的稍微走一遍,大部分bug就能解决
    • 重构提高编程速度(Efficiency)添加新的功能时候顾虑少一点,思路清晰
  4. 何时重构
    • 事不过三,三则重构
    • 预备性重构:添加新的功能时更加容易,让修改多处的代码变成修改一处
    • 帮助理解的重构:使代码更容易懂,让代码做到一目了然
    • 捡垃圾式重构:复审代码时感觉不好 如果有时间就改了
    • 有计划的重构:一般都是有了重大问题
    • 长期重构:先把要重构的地方放着,如果有人遇到要重构的地方就改,因为小修改后系统功能不变
  5. 何时不该重构
    • 既有代码太混乱,且不能正常工作,需要重写而不是重构。
    • 如果不需要修改那些代码就不要重构。
    • 项目接近最后期限时,应该避免重构。
  6. 重构的目标
为什么程序如此难以相与?设计与重构的目标
难以阅读的程序,难以修改容易阅读
逻辑重复的程序,难以修改所有逻辑都只在唯一地点指定
添加新行为时需要修改已有代码的程序,难以修改新的改动不会危及现有行为
带复杂条件逻辑的程序,难以修改尽可能简单表达条件逻辑
  1. 代码应该有一套完整的测试套件,并且运行速度要快。
  2. 先写出可调优的软件,然后调优它以求得足够的速度。

第三章 代码的坏味道

我觉得这一章的内容非常重要,识别出代码的坏味道,是开始正确重构的前提。

  1. 神秘命名
    命名这东西刚开始学编程的时候就很是个问题,abcd,xxx1234,拼音缩写等等,什么妖魔鬼怪都有,看别人的代码看到这些东西真的会头大。我自己一开始也用a1、a2什么的,过了几天就不知道我在写什么了。一个好名字能清晰表明自己的功能和用法。
    如果你想不出什么好名字,有可能背后还隐藏着更深的设计问题。

  2. 重复代码
    如果要修改重复代码,必须找出所有相关的副本来修改,想想就很累,还很容易出错,需设法提炼成函数。

  3. 过长函数
    函数越长,越难理解。给小函数良好的命名,阅读代码的人就可以通过名字了解函数的作用,根本不必去看其中写了什么。妙啊!
    每当感觉方法的某个地方需要注释来加以说明,可以把这部分代码放入一个独立的方法中,并以用途(而不是实现手法)来命名方法。
    条件表达式和循环常常也是提炼的信号。

  4. 过长参数列表
    不用参数就只能选择全局数据,这肯定是不可取的。
    改善的几点方法:

    • 如果可以向某个参数发起查询获得另一个参数的值,就用以查询取代参数。
    • 如果正在从现有的数据结构中抽取很多数据项,就保持对象完整。
    • 如果几个参数总是同时出现,就用引入参数对象。
    • 如果某个参数被用作区分函数行为的标记,可以使用移除标记参数。
  5. 全局数据
    全局数据的问题在于:从代码库任何一个角落都可以修改它。
    把全局数量用一个函数包装起来,并控制对其的访问,最好搬移到一个类或者模块中,控制其作用域。

  6. 可变数据
    在一处更新数据,却没有意识到软件中另一处期望着完全不同的数据,于是一个功能失效了。
    函数式编程–建立在“数据永不改变”的概念基础上:如果要更改一个数据结构,就返回一份新的数据副本,旧的数据仍保持不变。

  7. 发散式变化
    因为不同的原因,在不同的方向上,修改同一个模块。
    最近有点体会,多层结构系统,往往容易把全部逻辑都放在Service层,导致Service类非常庞大且不断被修改。

  8. 霰弹式修改
    每遇到某种变化,需要在多个类内做出许多小修改,容易遗漏。应该把需要修改的部分放到一处。

  9. 依恋情结
    函数和另一个模块中的函数或者数据交流频繁,远多于自己所处模块内部交流。最好将此函数移动到那个模块中。

  10. 数据泥团
    在很多地方看到相同的三四项数据,如果删掉其中一项,其他数据也没有意义,那就应该为它们产生一个新的对象。

  11. 基本类型偏执
    创建和自己的问题域有用的基本类型,不要简单用字符串等替代。

  12. 重复switch
    每当想要增加一个选择分支,必须找到所有的switch,并逐一更新。可以使用多态来解决。

  13. 循环语句
    用管道代替循环可以帮助我们更快看清被处理的元素以及处理它们的动作。

  14. 冗赘的元素
    如果一个类不值得存在,那么它就应该消失。

  15. 夸夸其谈通用性
    如果函数和类的唯一用户是测试案例,那就先删掉测试,然后移除死代码。

  16. 临时字段
    类中某个字段只为某些特殊情况而设置。

  17. 过长的消息链
    一个对象请求另一个对象,然后再请求另一个对象。。。代码与查找过程中的导航结构紧密耦合,一旦对象之间的关系发生任何变化,代码就不得不发生改变。

  18. 中间人
    某个类的接口有一半的函数都委托给其他类,就应该移除这个中间人。

  19. 内幕交易
    模块之间的数据交换很难完全避免,应该都放到明面上来。

  20. 过大的类
    类的设计应当遵循单一职责原则。

  21. 异曲同工的类
    类的替换要保持接口一致。

  22. 纯数据类
    把数据处理搬移到纯数据类中,除非被用作const返回值。

  23. 被拒绝的遗赠
    子类继承父类的所有函数和数据,子类只挑选几样来使用。为子类新建一个兄弟类,再运用下移方法和下移字段把用不到的函数下推个兄弟类。
    子类只复用了父类的行为,却不想支持父类的接口。运用委托替代继承来达到目的。

  24. 注释
    注释不是用来补救劣质代码的,事实上如果我们去除了代码中的所有坏味道,当劣质代码都被移除的时候,注释已经变得多余,因为代码已经讲清楚了一切。

第四章 构筑测试体系

  • 要正确地进行重构,前提是有一套稳固的测试集合,以帮助我发现难以避免的疏漏。
  • 编写优良的测试程序,可以极大提高编程速度。
  • 我们一开始写一些代码喜欢把结果输出到屏幕上 然后逐一检测,这些完全可以让计算机来做,我们要做的就是把期望的输出放到测试代码中,然后做一个对比就行了。
  • 编写测试代码其实就是在自己:为了添加功能我需要实现些什么?还能帮我把注意力剧种到接口而非实现。
  • 测试驱动开发----先编写一个失败的测试,编写代码使测试通过,然后进行重构以保证代码整洁。

其他章节建议看书,我感觉书上内容已经不太能够压缩了,等我以后有更多的感悟再来记录。

百度云链接

完整代码:https://pan.baidu.com/s/11AVSyjZcCgwby8BTxM_4uw
提取码:0nil

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第1章 重构,第一个案例 1 1.1 起点 1 1.2 重构的第一步 7 1.3 分解并重组statement() 8 1.4 运用多态取代与价格相关的条件逻辑 34 1.5 结语 52 第2章 重构原则 53 2.1 何谓重构 53 2.2 为何重构 55 2.3 何时重构 57 2.4 怎么对经理说 60 2.5 重构的难题 62 2.6 重构设计 66 2.7 重构与性能 69 2.8 重构起源何处 71 第3章 代码的坏味道 75 3.1 Duplicated Code(重复代码) 76 3.2 Long Method(过长函数) 76 3.3 Large Class(过大的类) 78 3.4 Long Parameter List(过长参数列) 78 3.5 Divergent Change(发散式变化) 79 3.6 Shotgun Surgery(霰弹式修改) 80 3.7 Feature Envy(依恋情结) 80 3.8 Data Clumps(数据泥团) 81 3.9 Primitive Obsession(基本类型偏执) 81 3.10 Switch Statements(switch惊悚现身) 82 3.11 Parallel InheritanceHierarchies(平行继承体系) 83 3.12 Lazy Class(冗赘类) 83 3.13 Speculative Generality(夸夸其谈未来性) 83 3.14 Temporary Field(令人迷惑的暂时字段) 84 3.15 Message Chains(过度耦合的消息链) 84 3.16 Middle Man(中间人) 85 3.17 Inappropriate Intimacy(狎昵关系) 85 3.18 Alternative Classes with Different Interfaces(异曲同工的类) 85 3.19 Incomplete Library Class(不完美的库类) 86 3.20 Data Class(纯稚的数据类) 86 3.21 Refused Bequest(被拒绝的遗赠) 87 3.22 Comments(过多的注释) 87 第4章 构筑测试体系 89 4.1 自测试代码的价值 89 4.2 JUnit测试框架 91 4.3 添加更多测试 97 第5章 重构列表 103 5.1 重构的记录格式 103 5.2 寻找引用点 105 5.3 这些重构手法有多成熟 106 第6章 重新组织函数 109 6.1 Extract Method(提炼函数) 110 6.2 Inline Method(内联函数) 117 6.3 Inline Temp(内联临时变量) 119 6.4 Replace Temp with Query(以查询取代临时变量) 120 6.5 Introduce Explaining Variable(引入解释性变量) 124 6.6 Split Temporary Variable(分解临时变量) 128 6.7 Remove Assignments to Parameters(移除对参数的赋值) 131 6.8 Replace Method with Method Object(以函数对象取代函数) 135 6.9 Substitute Algorithm(替换算法) 139 第7章 在对象之间搬移特性 141 7.1 Move Method(搬移函数) 142 7.2 Move Field(搬移字段) 146 7.3 Extract Class(提炼类) 149 7.4 Inline Class(将类内联化) 154 7.5 Hide Delegate(隐藏“委托关系”) 157 7.6 Remove Middle Man(移除中间人) 160 7.7 Introduce Foreign Method(引入外加函数) 162 7.8 Introduce Local Extension(引入本地扩展) 164 第8章 重新组织数据 169 8.1 Self Encapsulate Field(自封装字段) 171 8.2 Replace Data Value with Object(以对象取代数据值) 175 8.3 Change Value to Reference(将值对象改为引用对象) 179 8.4 Change Reference to Value(将引用对象改为值对象) 183 8.5 Replace Array with Object(以对象取代数组) 186 8.6 Duplicate Observed Data(复制“被监视数据”) 189 8.7 Change Unidirectional Association to Bidirectional(将单向关联改为双向关联) 197 8.8 Change Bidirectional Association to Unidirectional(将双向关联改为单向关联) 200 8.9 Replace Magic Number with Symbolic Constant(以字面常量取代魔法数) 204 8.10 Encapsulate Field(封装字段) 206 8.11 Encapsulate Collection(封装集合) 208 8.12 Replace Record with Data Class(以数据类取代记录) 217 8.13 Replace Type Code with Class(以类取代类型码) 218 8.14 Replace Type Code with Subclasses(以子类取代类型码) 223 8.15 Replace Type Code with State/Strategy(以State/Strategy取代类型码) 227 8.16 Replace Subclass with Fields(以字段取代子类) 232 第9章 简化条件表达式 237 9.1 Decompose Conditional(分解条件表达式) 238 9.2 Consolidate Conditional Expression(合并条件表达式) 240 9.3 Consolidate Duplicate Conditional Fragments(合并重复的条件片段) 243 9.4 Remove Control Flag(移除控制标记) 245 9.5 Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式) 250 9.6 Replace Conditional with Polymorphism(以多态取代条件表达式) 255 9.7 Introduce Null Object(引入Null对象) 260 9.8 Introduce Assertion(引入断言) 267 第10章 简化函数调用 271 10.1 Rename Method(函数改名) 273 10.2 Add Parameter(添加参数) 275 10.3 Remove Parameter(移除参数) 277 10.4 Separate Query from Modifier(将查询函数和修改函数分离) 279 10.5 Parameterize Method(令函数携带参数) 283 10.6 Replace Parameter with Explicit Methods(以明确函数取代参数) 285 10.7 Preserve Whole Object(保持对象完整) 288 10.8 Replace Parameter with Methods(以函数取代参数) 292 10.9 Introduce Parameter Object(引入参数对象) 295 10.10 Remove Setting Method(移除设值函数) 300 10.11 Hide Method(隐藏函数) 303 10.12 Replace Constructor with Factory Method(以工厂函数取代构造函数) 304 10.13 Encapsulate Downcast(封装向下转型) 308 10.14 Replace Error Code with Exception(以异常取代错误码) 310 10.15 Replace Exception with Test(以测试取代异常) 315 第11章 处理概括关系 319 11.1 Pull Up Field(字段上移) 320 11.2 Pull Up Method(函数上移) 322 11.3 Pull Up Constructor Body(构造函数本体上移) 325 11.4 Push Down Method(函数下移) 328 11.5 Push Down Field(字段下移) 329 11.6 Extract Subclass(提炼子类) 330 11.7 Extract Superclass(提炼超类) 336 11.8 Extract Interface(提炼接口) 341 11.9 Collapse Hierarchy(折叠继承体系) 344 11.10 Form Tem Plate Method(塑造模板函数) 345 11.11 Replace Inheritance with Delegation(以委托取代继承) 352 11.12 Replace Delegation with Inheritance(以继承取代委托) 355 第12章 大型重构 359 12.1 Tease Apart Inheritance(梳理并分解继承体系) 362 12.2 Convert Procedural Design to Objects(将过程化设计转化为对象设计) 368 12.3 Separate Domain from Presentation(将领域和表述/显示分离) 370 12.4 Extract Hierarchy(提炼继承体系) 375 第13章 重构,复用与现实 379 13.1 现实的检验 380 13.2 为什么开发者不愿意重构他们的程序 381 13.3 再论现实的检验 394 13.4 重构的资源和参考资料 394 13.5 从重构联想到软件复用和技术传播 395 13.6 小结 397 13.7 参考文献 397 第14章 重构工具 401 14.1 使用工具进行重构 401 14.2 重构工具的技术标准 403 14.3 重构工具的实用标准 405 14.4 小结 407 第15章 总结 409
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值