来自《重构,改善既有代码的设计(第二版)》
在原来的基础上加了点个人注解,并且还有一些使用 VS Code
进行重构的小技巧,希望可以帮助后来者的学习
简单例子
这个简单的例子是我后来加上的(因为感觉原书上的例子还是太过复杂,对新手很不友好),希望该例子能使读者感受到重构的魅力 ✊
function foo(arr) {
let sum = 123
console.log('-----------------------')
arr.forEach(e => sum += e)
console.log(`current sum is ${sum}`)
sum += 123
console.log(`current sum is ${sum}`)
}
foo([1, 2, 3, 4, 5])
❗️ 希望阅读下面文章之前,先以自己的想法进行重构,看看效果如何
有时,变量的初始化
与使用
之间有一定距离时,使用slide statements(移动语句)
将其放到一起
after slide statement
function foo(arr) {
console.log('-----------------------')
let sum = 123 // <----------- slide statement
arr.forEach(e => sum += e)
console.log(`current sum is ${sum}`)
sum += 123
console.log(`current sum is ${sum}`)
}
重构的精髓在于:小步修改
每次修改之后,就运行测试用例,如果你翻下错误,很容易便可发现它。
一个变量承担多个责任
,可以使用split variable (拆分变量)
创建临时变量
function foo(arr) {
console.log('-----------------------')
let sum = 123
arr.forEach(e => sum += e)
console.log(`current sum is ${sum}`)
const sum2 = sum + 123 // <----------- split variable --- sum2 为创建的临时常量
console.log(`current sum is ${sum2}`)
}
观察如上代码,一段代码同时处理两件不同的事,即处理数据的逻辑
与输出数据的逻辑
,那么,如何将其分离?
利用intermediate data(中转数据)
首先,使用slide statements
将相似逻辑的代码移动到一起
function foo(arr) {
let sum = 123
arr.forEach(e => sum += e)
const sum2 = sum + 123
console.log('-----------------------')
console.log(`current sum is ${sum}`)
console.log(`current sum is ${sum2}`)
}
使用extract function(提取函数)
将处理数据的逻辑
与输出数据的逻辑
提取出来
function foo(arr) {
const intermediateData = getData()
logInfo(intermediateData)
function getData() {
let sum = 123
arr.forEach(e => sum += e)
const sum2 = sum + 123
return { sum, sum2 } // <------- getData 返回的对象即 intermediate data: 包含了(输出数据的逻辑)所需的全部数据
}
function logInfo(data) {
console.log('-----------------------')
console.log(`current sum is ${data.sum}`)
console.log(`current sum is ${data.sum2}`)
}
}
可以看到,intermediateData
是一个对象,包含了logInfo
所需的全部数据
因为只有一处使用到了 intermediateData
,所以可以使用Inline Variable(内联变量)
:将表达式直接当作变量(常量)使用
function foo(arr) {
logInfo(getData()) // <----- 将表达式直接当作变量(常量)使用
function getData() {
let sum = 123
arr.forEach(e => sum += e)
const sum2 = sum + 123
return { sum, sum2 }
}
function logInfo(data) {
console.log('-----------------------')
console.log(`current sum is ${data.sum}`)
console.log(`current sum is ${data.sum2}`)
}
}
随着业务逻辑的增加,getData
的代码可能会越来越多,于是,你可以视情况使用extract function
提取不同逻辑,甚至放入不同的文件中
function foo(arr) {
logInfo(getData())
function getData() {
const sum = getTotal()
const sum2 = sum + 123
return { sum, sum2 }
function getTotal() {
let res = 123
arr.forEach(e => res += e)
return res
}
}
function logInfo(data) {
console.log('-----------------------')
logCurSum(data)
function logCurSum(data) {
Object.keys(data).forEach(key =>
console.log(`current sum is ${data[key]}`)
)
}
}
}
题目
有个名叫 BigCo
的顾客,选择了三个戏剧,分别为 Hamlet,As You Like It,Othello
编写一个函数function statement(invoice, plays)
,其中 invoice
为顾客的账单,plays
为所有戏剧的信息
可输出内容如下
原书题目描述如下
设想有一个戏剧演出团
,演员们经常要去各种场合表演戏剧
通常客户(customer)
会指定几出剧目,而剧团则根据观众(audience)人数
及剧目类型
来向客户收费。
该团目前出演两种戏剧:悲剧(tragedy)
和喜剧(comedy)
给客户发出账单时,剧团还会根据到场观众的数量
给出观众量积分(volume credit)
优惠
下次客户再请剧团表演时可以使用积分获得折扣——你可以把它看作一种提升客户忠诚度的方式
控制台彩色文字输出参考: nodejs 控制台彩色文字输出
初始代码
// 改变输出颜色的工具代码
function changeColor96(input: string | number): string {
return `\x1b[96m${input}\x1b[0m`
}
function changeColor95(input: string | number): string {
return `\x1b[95m${input}\x1b[0m`
}
// 所有戏剧信息,每个戏剧有(名称及类型)两个字段
const allPlaysInformation= {
"hamlet": { "name": "Hamlet", "type": "tragedy" },
"as-like": { "name": "As You Like It", "type": "comedy" },
"othello": { "name": "Othello", "type": "tragedy" }
}
// 账单(发票)
const invoiceData = {
"customer": "BigCo",
// customer 所选择的戏剧及该戏剧观众人数
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "as-like",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
type playType = {
name: string;
type: string;
}
type invoiceType = {
customer: string;
performances: {
playID: string;
audience: number;
}[];
}
// 打印账单详情
function statement(invoice: invoiceType, plays: { [playID: string]: playType }): string {
// 总金额
let totalAmount = 0;
// 总积分
let volumeCredits = 0;
let result = `Statement for ${changeColor95(invoice.customer)}\n`;
const format = new Intl.NumberFormat("en-US", {
style: "currency", currency: "USD",
minimumFractionDigits: 2
}).format;
for (let perf of invoice.performances) {
// 戏剧的具体信息: name 及 type
const play = plays[perf.playID];
// 单个戏剧总金额
let thisAmount = 0;
// ------------ 根据戏剧不同类型,决定收取多少金额 --------这段代码之后可提取到一个新函数中-------
switch (play.type) {
case "tragedy":
thisAmount = 40000;
// 悲剧人数 30+, 超出 1000/人
if (perf.audience > 30) {
thisAmount += 1000 * (perf.audience - 30);
}
break;
case "comedy":
thisAmount = 30000;
// 喜剧人数 20+, 超出 500/人
if (perf.audience > 20) {
thisAmount += 10000 + 500 * (perf.audience - 20);
}
// 喜剧另收 300/人
thisAmount += 300 * perf.audience;
break;
default:
throw new Error(`unknown type: ${play.type}`);
}
// ---------------------------------------------------------------------------------
// 添加观众量积分: 人数 30+, 超出 1人/1积分
volumeCredits += Math.max(perf.audience - 30, 0);
// 如果戏剧类型是 comedy ,另添 1人/0.2积分
if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);
// 输出单个戏剧的信息(名称,花费金额,观众人数)
result += ` ${changeColor95(play.name)}: ${changeColor96(format(thisAmount / 100))} (${perf.audience} seats)\n`;
// 计算总计花费
totalAmount += thisAmount;
}
result += `Amount owed is ${changeColor96(format(totalAmount / 100))}\n`;
result += `You earned ${changeColor96(volumeCredits)} credits\n`;
return result;
}
console.log(statement(invoiceData, allPlaysInformation));
提炼函数:提取 switch
一段较长的代码,过一段时间之后,我可能已经忘了这段代码的作用,因此需要重新查看其编写的逻辑
那么,是否能将这段代码提取到一个函数中?这样,仅仅根据函数名就可以了解这段代码的作用
小技巧:在
VS Code
中,选中该代码,点击电灯泡,即可选择重构的方式,之后可视情况进行调整
之后进行函数的更名:newFunction -> amountFor
做完这个改动后,马上编译并执行一遍测试,看看有无破坏了其他东西。
无论每次重构多么简单,养成重构后即运行测试的习惯非常重要。
做完一次修改就运行测试,这样在我真的犯了错时,只需要考虑一个很小的改动范围,这使得查错与修复问题易如反掌。
这就是重构过程的精髓所在:小步修改,每次修改后就运行测试。如果我改动了太多东西,犯错时就可能陷入麻烦的调试,并为此耗费大把时间。小步修改,以及它带来的频繁反馈,正是防止混乱的关键。
修改提炼出来的函数
将代码提炼到一个函数后,看看是否能进一步提升其表达能力
① 个人风格:永远将函数的返回值命名为 result ,这样一眼就可以知道它的作用
② 为参数取名时默认带上其类型名
这里取名叫做 aPerformance( 一个具体的戏剧名称) 是不是很容易理解呢
好的代码应该能清除地表明它在做什么,而变量命名时代码清晰的关键
小技巧:
VS Code
中按F2
可整体修改变量名
更名内容如下
移除推断变量
在 for
循环里有这样一段代码,play
是由 invoice.performances
计算得到的,根本没必要将它作为参数传入
创建很多局部作用域的临时变量,将会使提炼函数变得更加复杂
比如累加总金额及总积分
都用到了play
常量,之后将其单出抽取出来,都要带着play
常量
将plays[perf.playID]
提取到一个函数中,将该函数的返回值赋值给play
发现常量play
可以直接用函数plays[perf.playID]
表示,于是,删除常量play
,直接使用函数plays[perf.playID]
修改 amount 函数
,将 amount 函数
中使用到 play
变量的地方全部替换为 playFor(aPerformace)
,之后删除amount函数
的第二个参数
在for循环
中,发现 amountFor(perf)
赋值给一个临时变量后,之后便不再修改,因此,直接将 amountFor(perf)
当做一个变量使用
提取 volumeCredits
循环的每次迭代都会更新 volumeCredits
变量的值,而更新 volumeCredits
变量的逻辑有点多,于是将这整块逻辑提炼到一个新函数中
并将新函数命名:newFuntion -> volumeCreditsFor
之后,重构为如下形式
移除 format
移除 format
代码变量:临时变量
只在内部代码块中
起作用,并使得你的代码变得更加复杂
将format
更名为use
,并将除以100的逻辑也搬到函数内部来,使用时不用像之前一样除以 100
拆分循环
一个循环身兼多职,会特别乱,比如该for循环
,又是增加积分,又是增加总金额
如果可以将一个循环拆分,每次修改时你值需要理解要修改的那块代码就可以了
拆分
移动
之后将变量声明移动到接近循环的位置
提炼函数
将声明与使用变量的地方集中到一起,有利于代码之后的重构
内联变量
只有一处使用到了 volumeCredits
变量,因此可将表达式直接当作变量使用
接下来以同样的方式处理totalAmount
当前代码
function changeColor96(input: string | number): string {
return `\x1b[96m${input}\x1b[0m`
}
function changeColor95(input: string | number): string {
return `\x1b[95m${input}\x1b[0m`
}
const allPlaysData = {
"hamlet": { "name": "Hamlet", "type": "tragedy" },
"as-like": { "name": "As You Like It", "type": "comedy" },
"othello": { "name": "Othello", "type": "tragedy" }
}
const invoiceData = {
"customer": "BigCo",
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "as-like",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
type playType = {
name: string;
type: string;
}
type invoiceType = {
customer: string;
performances: {
playID: string;
audience: number;
}[];
}
function statement(invoice: invoiceType, plays: { [playID: string]: playType }): string {
let res = `Statement for ${changeColor95(invoice.customer)}\n`;
for (let perf of invoice.performances) {
res += ` ${changeColor95(playFor(perf).name)}: ${changeColor96(usd(amountFor(perf)))} (${perf.audience} seats)\n`;
}
res += `Amount owed is ${changeColor96(usd(appleSauce()))}\n`;
res += `You earned ${changeColor96(totalVolumeCredits())} credits\n`;
return res;
function appleSauce() {
let res = 0
for (let perf of invoice.performances) {
res += amountFor(perf)
}
return res
}
function totalVolumeCredits() {
let res = 0
for (let perf of invoice.performances) {
res += volumeCreditsFor(perf)
}
return res
}
function usd(aNumber: number) {
return new Intl.NumberFormat("en-US", {
style: "currency", currency: "USD",
minimumFractionDigits: 2
}).format(aNumber / 100)
}
function volumeCreditsFor(aPerformance: { playID: string; audience: number }) {
let res = 0
res += Math.max(aPerformance.audience - 30, 0)
if ("comedy" === playFor(aPerformance).type)
res += Math.floor(aPerformance.audience / 5)
return res
}
function playFor(aPerformance: { playID: string; audience: number }) {
return plays[aPerformance.playID]
}
function amountFor(aPerformance: { playID: string; audience: number }) {
let res = 0
switch (playFor(aPerformance).type) {
case "tragedy":
res = 40000
if (aPerformance.audience > 30) {
res += 1000 * (aPerformance.audience - 30)
}
break
case "comedy":
res = 30000
if (aPerformance.audience > 20) {
res += 10000 + 500 * (aPerformance.audience - 20)
}
res += 300 * aPerformance.audience
break
default:
throw new Error(`unknown type: ${playFor(aPerformance).type}`)
}
return res
}
}
console.log(statement(invoiceData, allPlaysData));
⭐️ 将计算逻辑通过中转数据转移出去
将
计算逻辑
通过中转数据
转移出去,这句话本身就是重点
目前为止,重构着重聚焦在函数结构上
的修改,使得我通过函数名就可以理解函数内部逻辑
早期重构方式一般聚焦于将大块复杂代码块
拆分为更小的单元
现在,我们来聚焦在函数功能
上的部分,提供一个函数版本,使得在html
中也可以使用
早期,我们将计算的代码
从函数中分离出来,现在,我们只需要更改顶部的7行代码
就可以实现html版本
的了,可是我又想复用函数底部的逻辑代码,于是我将之前那顶部的7行代码
抽取出来
更名:statement
-> renderPlainText
创建一个新函数,引用renderPlainText
目前,在renderPlainText
函数中,有处理数据
的逻辑,也有使用数据
的逻辑
所以,我们要在renderPlainText
函数中保留使用数据
的逻辑,而将处理数据
的逻辑移到statement
函数中
因此,我们要借助到中转数据
中转数据
观察如下数据,performace
的playId
与上面的数据相对应,因此,给performance
添加一个名为play
的常量
创建一个中转数据(一个对象)
,中转函数中存储所有renderPlainText 函数
需要使用的数据
给renderPlainText 函数
传参数时,只需要传入中转数据
一个参数就行
之后,以同样手法重构amountFor
, volumeCreditsFor
,appleSauce
, totalVolumeCredits
,将数据处理完成后,放入中转函数
中
function changeColor96(input: string | number): string {
return `\x1b[96m${input}\x1b[0m`
}
function changeColor95(input: string | number): string {
return `\x1b[95m${input}\x1b[0m`
}
const allPlaysData = {
"hamlet": { "name": "Hamlet", "type": "tragedy" },
"as-like": { "name": "As You Like It", "type": "comedy" },
"othello": { "name": "Othello", "type": "tragedy" }
}
const invoiceData = {
"customer": "BigCo",
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "as-like",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
type playType = {
name: string
type: string
}
type invoiceType = {
customer: string
performances: {
playID: string
audience: number
}[];
}
type aPerformanceType = {
playID: string
audience: number
play?: playType
amount?: number
volumeCredits?: number
}
type renderPlainTextTpye = {
customer: string
performances: aPerformanceType[];
totalAmount?: number
totalVolumeCredits?: number
}
function statement(invoice: invoiceType, plays: { [playID: string]: playType }): string {
const statementData: renderPlainTextTpye = {
customer: invoice.customer,
performances: invoice.performances.map(enrichPerformance)
}
statementData.totalAmount = totalAmount(statementData)
statementData.totalVolumeCredits = totalVolumeCredits(statementData)
return renderPlainText(statementData)
function enrichPerformance(aPerformance: aPerformanceType) {
const res: aPerformanceType = Object.assign({}, aPerformance)
res.play = playFor(res)
res.amount = amountFor(res)
res.volumeCredits = volumeCreditsFor(res)
return res
}
function playFor(aPerformance: invoiceType['performances'][0]) {
return plays[aPerformance.playID]
}
function volumeCreditsFor(aPerformance: aPerformanceType) {
let res = 0
res += Math.max(aPerformance.audience - 30, 0)
if ("comedy" === aPerformance.play!.type)
res += Math.floor(aPerformance.audience / 5)
return res
}
function amountFor(aPerformance: aPerformanceType) {
let res = 0
switch (aPerformance.play!.type) {
case "tragedy":
res = 40000
if (aPerformance.audience > 30) {
res += 1000 * (aPerformance.audience - 30)
}
break
case "comedy":
res = 30000
if (aPerformance.audience > 20) {
res += 10000 + 500 * (aPerformance.audience - 20)
}
res += 300 * aPerformance.audience
break
default:
throw new Error(`unknown type: ${aPerformance.play!.type}`)
}
return res
}
function totalAmount(data: renderPlainTextTpye) {
let res = 0
for (let perf of data.performances) {
res += perf.amount!
}
return res
}
function totalVolumeCredits(data: renderPlainTextTpye) {
let res = 0
for (let perf of data.performances) {
res += perf.volumeCredits!
}
return res
}
}
function renderPlainText(data: renderPlainTextTpye): string {
let res = `Statement for ${changeColor95(data.customer)}\n`;
for (let perf of data.performances) {
res += ` ${changeColor95(perf.play!.name)}: ${changeColor96(usd(perf.amount!))} (${perf.audience} seats)\n`;
}
res += `Amount owed is ${changeColor96(usd(data.totalAmount!))}\n`;
res += `You earned ${changeColor96(data.totalVolumeCredits!)} credits\n`;
return res;
function usd(aNumber: number) {
return new Intl.NumberFormat("en-US", {
style: "currency", currency: "USD",
minimumFractionDigits: 2
}).format(aNumber / 100)
}
}
console.log(statement(invoiceData, allPlaysData));
管道取代循环
function totalAmount(data: renderPlainTextTpye) {
return data.performances.reduce((total, p) => total + p.amount!, 0);
}
function totalVolumeCredits(data: renderPlainTextTpye) {
return data.performances.reduce((total, p) => total + p.volumeCredits!, 0);
}
将创建中转数据的代码及对应函数单独提取出来
function statement(invoice: invoiceType, plays: { [playID: string]: playType }): string {
return renderPlainText(createStatementData(invoice, plays))
}
function createStatementData(invoice: invoiceType, plays: { [playID: string]: playType }) {
const statementData: renderPlainTextTpye = {
customer: invoice.customer,
performances: invoice.performances.map(enrichPerformance)
}
statementData.totalAmount = totalAmount(statementData)
statementData.totalVolumeCredits = totalVolumeCredits(statementData)
return statementData
// ......
}
提炼成两个文件
createStatementData.js
文件代码如下
import type { invoiceType, playType, aPerformanceType, renderPlainTextTpye } from './index'
export default function createStatementData(invoice: invoiceType, plays: { [playID: string]: playType }) {
// ......
}
import createStatementData from './createStatementData'
function statement(invoice: invoiceType, plays: { [playID: string]: playType }): string {
return renderPlainText(createStatementData(invoice, plays))
}
html 版本使用
function htmlStatement(invoice: invoiceType, plays: { [playID: string]: playType }): string {
return renderHtml(createStatementData(invoice, plays));
}
function renderHtml(data: renderPlainTextTpye): string {
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>`;
result += `<td>${usd(perf.amount!)}</td></tr>\n`;
}
result += "</table>\n";
result += `<p>Amount owed is <em>${usd(data.totalAmount!)}</em></p>\n`;
result += `<p>You earned <em>${data.totalVolumeCredits}</em> credits</p>\n`;
return result;
}
当前代码
index.ts
import { changeColor95, changeColor96 } from './utils'
import { allPlaysData, invoiceData } from './data'
import type { invoiceType, playType, renderPlainTextTpye } from './type'
import createStatementData from './createStatementData'
function usd(aNumber: number) {
return new Intl.NumberFormat("en-US", {
style: "currency", currency: "USD",
minimumFractionDigits: 2
}).format(aNumber / 100)
}
function statement(invoice: invoiceType, plays: { [playID: string]: playType }): string {
return renderPlainText(createStatementData(invoice, plays))
}
function renderPlainText(data: renderPlainTextTpye): string {
let res = `Statement for ${changeColor95(data.customer)}\n`;
for (let perf of data.performances) {
res += ` ${changeColor95(perf.play!.name)}: ${changeColor96(usd(perf.amount!))} (${perf.audience} seats)\n`;
}
res += `Amount owed is ${changeColor96(usd(data.totalAmount!))}\n`;
res += `You earned ${changeColor96(data.totalVolumeCredits!)} credits\n`;
return res;
}
function htmlStatement(invoice: invoiceType, plays: { [playID: string]: playType }): string {
return renderHtml(createStatementData(invoice, plays));
}
function renderHtml(data: renderPlainTextTpye): string {
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>`;
result += `<td>${usd(perf.amount!)}</td></tr>\n`;
}
result += "</table>\n";
result += `<p>Amount owed is <em>${usd(data.totalAmount!)}</em></p>\n`;
result += `<p>You earned <em>${data.totalVolumeCredits}</em> credits</p>\n`;
return result;
}
console.log(htmlStatement(invoiceData, allPlaysData));
utils.ts
export function changeColor96(input: string | number): string {
return `\x1b[96m${input}\x1b[0m`
}
export function changeColor95(input: string | number): string {
return `\x1b[95m${input}\x1b[0m`
}
data.ts
export const allPlaysData = {
"hamlet": { "name": "Hamlet", "type": "tragedy" },
"as-like": { "name": "As You Like It", "type": "comedy" },
"othello": { "name": "Othello", "type": "tragedy" }
}
export const invoiceData = {
"customer": "BigCo",
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "as-like",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
type.ts
export type playType = {
name: string
type: string
}
export type invoiceType = {
customer: string
performances: {
playID: string
audience: number
}[];
}
export type aPerformanceType = {
playID: string
audience: number
play?: playType
amount?: number
volumeCredits?: number
}
export type renderPlainTextTpye = {
customer: string
performances: aPerformanceType[];
totalAmount?: number
totalVolumeCredits?: number
}
createStatementData.ts
import type { invoiceType, playType, aPerformanceType, renderPlainTextTpye } from './type'
export default function createStatementData(invoice: invoiceType, plays: { [playID: string]: playType }) {
const statementData: renderPlainTextTpye = {
customer: invoice.customer,
performances: invoice.performances.map(enrichPerformance)
}
statementData.totalAmount = totalAmount(statementData)
statementData.totalVolumeCredits = totalVolumeCredits(statementData)
return statementData
function enrichPerformance(aPerformance: aPerformanceType) {
const res: aPerformanceType = Object.assign({}, aPerformance)
res.play = playFor(res)
res.amount = amountFor(res)
res.volumeCredits = volumeCreditsFor(res)
return res
}
function playFor(aPerformance: invoiceType['performances'][0]) {
return plays[aPerformance.playID]
}
function volumeCreditsFor(aPerformance: aPerformanceType) {
let res = 0
res += Math.max(aPerformance.audience - 30, 0)
if ("comedy" === aPerformance.play!.type)
res += Math.floor(aPerformance.audience / 5)
return res
}
function amountFor(aPerformance: aPerformanceType) {
let res = 0
switch (aPerformance.play!.type) {
case "tragedy":
res = 40000
if (aPerformance.audience > 30) {
res += 1000 * (aPerformance.audience - 30)
}
break
case "comedy":
res = 30000
if (aPerformance.audience > 20) {
res += 10000 + 500 * (aPerformance.audience - 20)
}
res += 300 * aPerformance.audience
break
default:
throw new Error(`unknown type: ${aPerformance.play!.type}`)
}
return res
}
function totalAmount(data: renderPlainTextTpye) {
return data.performances.reduce((total, p) => total + p.amount!, 0);
}
function totalVolumeCredits(data: renderPlainTextTpye) {
return data.performances.reduce((total, p) => total + p.volumeCredits!, 0);
}
}
现在对代码进行拓展,加入更多的戏剧类型
每个类型有其各自计算金额与积分的方式
现在,我只需要在amountFor
里,修改switch
那块的代码,可是switch
代码中包含一些重复逻辑,如何才能复用它呢?
这里,使用到了一种叫多态
的方式,创建一个公共类
,用于存放相同逻辑的代码,而其子类
则实现不同的部分
创建 Performance Calculator
enrichPerformance 函数
下方的逻辑主要是计算,因此,将其搬到一个名为PerformanceCalculator
的类中更为合适
class PerformanceCalculator {
constructor(public performance: aPerformanceType, public play: playType) {
}
get amount() {
let res = 0
switch (this.play.type) {
case "tragedy":
res = 40000
if (this.performance.audience > 30) {
res += 1000 * (this.performance.audience - 30)
}
break
case "comedy":
res = 30000
if (this.performance.audience > 20) {
res += 10000 + 500 * (this.performance.audience - 20)
}
res += 300 * this.performance.audience
break
default:
throw new Error(`unknown type: ${this.play.type}`)
}
return res
}
get volumeCredits() {
let res = 0
res += Math.max(this.performance.audience - 30, 0)
if ("comedy" === this.play.type)
res += Math.floor(this.performance.audience / 5)
return res
}
}
ts 中,public 默认指代 this
工厂函数
使用工厂函数的方式创建 PerformanceCalculator
实例
function createPerformanceCalculator(aPerformance: aPerformanceType, play: playType) {
return new PerformanceCalculator(aPerformance, play)
}
export default function createStatementData(/*......*/) {
// ......
return statementData
function enrichPerformance(aPerformance: aPerformanceType) {
const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance))
// ......
}
}
通过这种方式,我可以创建PerformanceCalculator
的子类
子类编写不同逻辑,并引用父类公共逻辑
function createPerformanceCalculator(aPerformance: aPerformanceType, aPlay: playType) {
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}`)
}
}
class TragedyCalculator extends PerformanceCalculator {
get amount() {
let res = 40000
if (this.performance.audience > 30) {
res += 1000 * (this.performance.audience - 30)
}
return res
}
}
class ComedyCalculator extends PerformanceCalculator {
get amount() {
let res = 30000
if (this.performance.audience > 20) {
res += 10000 + 500 * (this.performance.audience - 20)
}
res += 300 * this.performance.audience
return res
}
get volumeCredits() {
return super.volumeCredits + Math.floor(this.performance.audience / 5)
}
}
最终代码如下
import type { invoiceType, playType, aPerformanceType, renderPlainTextTpye } from './type'
class PerformanceCalculator {
constructor(public performance: aPerformanceType, public play: playType) {
}
get amount():number {
throw Error('subclass responsibility')
}
get volumeCredits() {
return Math.max(this.performance.audience - 30, 0)
}
}
function createPerformanceCalculator(aPerformance: aPerformanceType, aPlay: playType) {
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}`)
}
}
class TragedyCalculator extends PerformanceCalculator {
get amount() {
let res = 40000
if (this.performance.audience > 30) {
res += 1000 * (this.performance.audience - 30)
}
return res
}
}
class ComedyCalculator extends PerformanceCalculator {
get amount() {
let res = 30000
if (this.performance.audience > 20) {
res += 10000 + 500 * (this.performance.audience - 20)
}
res += 300 * this.performance.audience
return res
}
get volumeCredits() {
return super.volumeCredits + Math.floor(this.performance.audience / 5)
}
}
export default function createStatementData(invoice: invoiceType, plays: { [playID: string]: playType }) {
const statementData: renderPlainTextTpye = {
customer: invoice.customer,
performances: invoice.performances.map(enrichPerformance)
}
statementData.totalAmount = totalAmount(statementData)
statementData.totalVolumeCredits = totalVolumeCredits(statementData)
return statementData
function enrichPerformance(aPerformance: aPerformanceType) {
const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance))
const res: aPerformanceType = Object.assign({}, aPerformance)
res.play = calculator.play
res.amount = calculator.amount
res.volumeCredits = calculator.volumeCredits
return res
}
function playFor(aPerformance: invoiceType['performances'][0]) {
return plays[aPerformance.playID]
}
function totalAmount(data: renderPlainTextTpye) {
return data.performances.reduce((total, p) => total + p.amount!, 0);
}
function totalVolumeCredits(data: renderPlainTextTpye) {
return data.performances.reduce((total, p) => total + p.volumeCredits!, 0);
}
}