前言
今天,学习记录一下重构的第一章
原本实现代码
下面一段代码,传入一个演出的数据,计算打印出每一种类型节目的收费和观看人数,并计算出演出完之后的总费用和总观众积分
// 每一场演出的观众人数
const invoice = {
customer: 'BigCo',
performances: [
{
playID: 'hamlet',
audience: 55
},
{
playID: 'as-like',
audience: 35
},
{
playID: 'othello',
audience: 40
}
]
}
// 每场演出对应的戏剧类型和名字
const plays = {
hamlet: {
name: 'Hamlet',
type: 'tragedy'
},
'as-like': {
name: 'As You Like It',
type: 'comedy'
},
othello: {
name: 'Othello',
type: 'tragedy'
}
}
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(`unknow type: ${play.type}`)
}
volumeCredits += Math.max(perf.audience - 30, 0)
if (play.type === 'comedy') {
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
}
第一版优化
- 提取函数
- 减少临时变量
计算总的演出费用
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(`unknow type: ${playFor(aPerformance).type}`)
}
return result
}
// 提取函数:计算所有的节目演出费用
function totalVolumeCredits() {
let volumeCredits = 0
for (let perf of invoice.performances) {
volumeCredits += volumeCreditsFor(perf)
}
return volumeCredits
}
计算观众量积分
// 提取函数:计算每场观众量积分
function volumeCreditsFor(perf) {
let result = 0;
result += Math.max(perf.audience - 30, 0)
if (playFor(perf).type === 'comedy') {
result += Math.floor(perf.audience / 5)
}
return result
}
// 提取函数:计算总得观众量积分
function totalVolumeCredits() {
let volumeCredits = 0
for (let perf of invoice.performances) {
volumeCredits += volumeCreditsFor(perf)
}
return volumeCredits
}
打印结果
// format变量提取为函数
function usd(aNumner) {
return new Intl.NumberFormat('en-us', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2
}).format(aNumner / 100)
}
// 拼接结果
function renderPlainText(invoice) {
let result = `Statement for ${invoice.customer}\n`
for (let perf of invoice.performances) {
result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`
}
result += `Amount owed is ${usd(totalAmount())}\n`
result += `You earned ${totalVolumeCredits()} credits\n`
return result
}
// 执行入口
function statement(invoice) {
return renderPlainText(invoice)
}
第2版优化
将数据处理和字符串拼接分开处理,甚至可以分文件引入
// 演出费用
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(`unknow type: ${playFor(aPerformance).type}`)
}
return result
}
// 观众积分
function volumeCreditsFor(perf) {
let result = 0;
result += Math.max(perf.audience - 30, 0)
if (playFor(perf).type === 'comedy') {
result += Math.floor(perf.audience / 5)
}
return result
}
// 处理数据
function createStatementData(invoice) {
const result = {}
result.customer = invoice.customer
// 计算每一项的费用、观众积分、类型
result.performances = invoice.performances.map(v => {
const result = Object.assign({}, v)
result.play = playFor(result)
result.amount = amountFor(result)
result.volumeCredits = volumeCreditsFor(result)
return result
})
// 总费用
result.totalAmount = result.performances.reduce((total, p) => total + p.amount, 0)
// 总观众积分
result.totalVolumeCredits = result.performances.reduce((total, p) => total + p.volumeCredits, 0)
return result
}
// 拼接文本
function renderPlainText(data) {
let result = `Statement for ${data.customer}\n`
for (let perf of data.performances) {
result += ` ${data.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`
}
result += `Amount owed is ${usd(data.totalAmount)}\n`
result += `You earned ${data.totalVolumeCredits} credits\n`
return result
}
第三版
从代码的可维护性:针对不同节目类型,通过类来分别计算费用和观众积分,便于后期维护和新增喜剧类型
抽出一个演出节目的基类,用于不同类型戏剧的继承
class PerformanceCalculator {
constructor(aPerformance, aPlay) {
this.performance = aPerformance
this.play = aPlay
}
// 费用由子方法内部实现
get amount() {
throw new Error(`subclass responsibility`)
}
// 计算观众积分的通用求值
get volumeCredits() {
return Math.max(this.performance.audience - 30, 0)
}
}
抽取悲剧类
- 观众积分求值用父类,自己实现了费用计算方法
class TragedyCalculator extends PerformanceCalculator {
get amount() {
let result = 40000;
if (this.performance.audience > 30) {
result += 1000 * (this.performance.audience - 30)
}
return result
}
}
抽取喜剧类
class ComedyCalculator extends PerformanceCalculator {
get amount() {
let result = 30000;
if (this.performance.audience > 20) {
result += 10000 + 500 * (this.performance.audience - 20)
}
result += 300 * this.performance.audience
return result
}
get volumeCredits() {
return super.volumeCredits + Math.floor(this.performance.audience / 5)
}
}
创建戏剧计算器
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}`)
}
}
方法调用
// 处理数据
function createStatementData(invoice, plays) {
const result = {}
result.customer = invoice.customer
// 计算每一项的演出费用、观众积分
result.performances = invoice.performances.map(v => {
// 调用创建演出戏剧的实例
const calculator = createPerformanceCalculator(v, plays[v.playID])
const result = Object.assign({}, v)
result.play = calculator.play
result.amount = calculator.amount
result.volumeCredits = calculator.volumeCredits
return result
})
result.totalAmount = result.performances.reduce((t, p) => t + p.amount, 0)
result.totalVolumeCredits = result.performances.reduce((t, p) => t + p.volumeCredits, 0)
return result
}
// 格式化数据成最终字符串结果
function renderPlainText(data) {
let result = `Statement for ${data.customer}\n`
for (let perf of data.performances) {
result += ` ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`
}
result += `Amount owed is ${usd(data.totalAmount)}\n`
result += `You earned ${data.totalVolumeCredits} credits\n`
return result
}
// 函数调用
function statement(invoice, plays) {
return renderPlainText(createStatementData(invoice, plays))
}
最后
学习记录《重构2》的第一章,用到了提取函数、减少临时变量、搬移函数、多态取代条件表达式等方法。直接看好像代码量变多了,数据多循环了几次,但是代码可维护性变低了,在数据量小的情况下,多循环的次数造成的性能可以忽略不计。印象深刻的是一句话是:编程时,需要遵循营地法则:保证你离开时代码库一定比你来时更健康