一、需求
为戏剧演出团实现一套剧目表演收费逻辑,输出收费单
费用计算:根据观众人数、剧目类型收费,同时减去相应的观众量积分
观众量积分:根据到场观众人数给出,客户用户下次付款时抵扣
二、改造前代码
存在问题:
1、 代码组织不甚清晰
2、 如果打印的结果展现形式有变,如使用HTML 格式输出账单,则 返回结果 result 需要做多个分支逻辑,或者复制多一份代码,用以返回html 格式的result
3、 如果剧目类型增加,则戏剧场次的计费方式、积分计算方式不同,代码会变得更加冗长
/*
* @Name: Index
* @Description: 打印账单详情, 根据观众人数和剧目类型收费, 发出账单时还会根据观众人数给出客户积分
* @Copyright: 广州银云信息科技有限公司
* @LastEditors: lisp
* @LastEditTime: 2022-06-03 14:07:27
*/
const plays = require("./data/plays")
const invoice = require("./data/invoice")
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`
console.log(result)
return result
}
statement(invoice, plays)
/*
Statement for BigCo
Hamlet: $650.00 (55 seats)
As You Like It: $580.00 (35 seats)
Othello: $500.00 (40 seats)
Amount owed is $1,730.00
You earned 47 credits
*/
三、改造后代码
把主要计算逻辑封装在createStatementData.js 文件里
主要解决问题
- 抽取与主函数业务逻辑关联性不强的代码为独立函数,使主函数结构清晰明了
- 独立函数严格遵循单一功能原则,一个函数只做一件事
- 把计算剧目表演相关的逻辑封装成类,建立一套继承体系,类的多态功能代替多重计算分支,由子类来实现主要主要计算金额、积分功能,如若需增加剧目类型,只需增加子类即可
输出打印单–改造后–statement.js
/*
* @Name:
* @Description:
* @Copyright: 广州银云信息科技有限公司
* @LastEditors: lisp
* @LastEditTime: 2022-06-03 17:41:31
*/
module.exports = createStatementData = (invoice, plays) => {
const result = {}
result.customer = invoice.customer
result.performances = invoice.performances.map(enrichPerformance)
result.totalAmount = calcAmountTotal(result)
result.totalVolumeCredits = calcVolumeCreditsTotal(result)
return result
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
}
function playFor(aPerformance) {
return plays[aPerformance.playID]
}
function calcAmountTotal(data) {
return data.performances.reduce((total, p) => total + p.amount, 0)
}
function calcVolumeCreditsTotal(data) {
return data.performances.reduce((total, p) => total + p.volumeCredits, 0)
}
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}`)
}
}
}
class PerformanceCalculator {
constructor(aPerformance, aPlay) {
this.performance = aPerformance
this.play = aPlay
}
get amount() {
return 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 = 40000
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)
}
}