重构,第一个示例

来自《重构,改善既有代码的设计(第二版)》

在原来的基础上加了点个人注解,并且还有一些使用 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函数中

因此,我们要借助到中转数据

中转数据

观察如下数据,performaceplayId与上面的数据相对应,因此,给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);
  }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值