CH01_重构、第一个示例

概述

在这一章节,作者给出了一个戏剧演出团售票的示例:剧目有悲剧(tragedy)和喜剧(comedy);为了卖出更多的票,剧团则更具观众的数量来为下次演出打折扣(大致意思是这次的观众越多,下次的票价越低)

设计

采用JSON存储数据(因为代码是用Javascript写的),plays.json 存储剧目,invoices.json存储账单

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
		}
	]
}]

下面这个函数用于打印账单详情

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}`);
		}
		// add volume credits
		volumeCredits += Math.max(perf.audience - 30, 0);
		// add extra credit for every ten comedy attendees
		if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);
		// print line for this order
		result += ` ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`;
		totalAmount += thisAmount;
	}
	result += `Amount owed is ${format(totalAmount/100)}\n`;
	result += `You earned ${volumeCredits} credits\n`;
	return result;
}

函数statement实现的打印账单的功能,能正常工作;但是其结构“不甚清晰”。对于这个几十行的代码,我们要读懂、理解并不困难,如果业务比较复杂、函数很长(上百行)时(笔者在维护既有项目时见过2000多行的函数,阅读代码就像是在考古)再去阅读,或者让其他人阅读,要弄清逻辑确实需要花费一番功夫。

如果没有新的需求导入,statement保持现状也可忍受!

新的需求导入

  • 要求输出部分以HTML的格式
  • 扩充新的剧目类型(如:历史剧、田园剧等)
  • ……

需求会源源不断的导入,如果每次导入一个的需求,都在statement函数中修改,随着时间的推移,statement函数将变得臃肿不堪、难以阅读和理解;这也是我们工作中经常做的(目的是懒省事儿,需求来了,在既有的函数中改改能实现功能就完事)。

分解函数statement

  1. 提取amountFor函数(for循环中的代码,注意提取参数啊Performance、play)
  2. 提取playFor函数(play参数不是for循环的变量,而是计算出来的)
  3. 修改amountFor函数,减去play参数
  4. volumeCredits累加变量提取volumeCreditsFor函数
  5. 提取format函数(格式化数据)
  6. ……

具体过程参考《重构改善既有代码设计第二版》第一章

function statement(invoice, plays) {
	let result = `Statement for ${invoice.customer}\n`;
	for (let perf of invoice.performances) {
		result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} s
eats)\n`;
	}
	result += `Amount owed is ${usd(totalAmount())}\n`;
	result += `You earned ${totalVolumeCredits()} credits\n`;
	return result;

	function totalAmount() {
		let result = 0;
		for (let perf of invoice.performances) {
			result += amountFor(perf);
		}
		return result;
	}

	function totalVolumeCredits() {
		let result = 0;
		for (let perf of invoice.performances) {
			result += volumeCreditsFor(perf);
		}
		return result;
	}

	function usd(aNumber) {
		return new Intl.NumberFormat("en-US", {
				style: "currency",
				currency: "USD",
				minimumFractionDigits: 2
			})
			.format(aNumber / 100);
	}

	function volumeCreditsFor(aPerformance) {
		let result = 0;
		result += Math.max(aPerformance.audience - 30, 0);
		if ("comedy" === playFor(aPerformance)
			.type) result += Math.floor(aPerformance
			.audience / 5);
		return result;
	}

	function 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;
	}
}
  • 拆分计算阶段与格式化阶段
  • 分离到两个文件
  • ……

总结

一般来说,重构早期的主要动力是尝试理解代码如何工作。通常我们需要先通读代码,找到一些感觉,然后再通过重构将这些感觉从脑海里搬回到代码中。清晰的代码更容易理解,使你能够发现更深层次的设计问题,从而形成积极正向的反馈环。

所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减小整理过程中引入错误的概率。本质上说,重构就是在代码写好之后改进它的设计。

有了重构以后,工作的平衡点开始发生变化。设计不是在一开始完成的,而是在整个开发过程中逐渐浮现出来。在系统构筑过程中,我们需要学会如何不断改进设计。这个“构筑-设计”的反复互动,可以让一个程序在开发过程中持续保有良好的设计。

你可能感兴趣的:(重构)