多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
接下来我将注意力集中到下一个特性改动:支持更多类型的戏剧,以及支持它们各自的价格计算和观众量积分计算。对于现在的结构,我只需要在计算函数里添加分支逻辑即可。`amountFor`函数清楚地体现了,戏剧类型在计算分支的选择上起着关键的作用——但这样的分支逻辑很容易随代码堆积而腐坏,除非编程语言提供了更基础的编程语言元素来防止代码堆积。 要为程序引入结构、显式地表达出“计算逻辑的差异是由类型代码确定”有许多途径,不过最自然的解决办法还是使用面向对象世界里的一个经典特性——类型多态。传统的面向对象特性在JavaScript世界一直备受争议,但新的ECMAScript 2015规范有意为类和多态引入了一个相当实用的语法糖。这说明,在合适的场景下使用面向对象是合理的——显然我们这个就是一个合适的使用场景。 我的设想是先建立一个继承体系,它有“喜剧”(`comedy`)和“悲剧”(`tragedy`)两个子类,子类各自包含独立的计算逻辑。调用者通过调用一个多态的`amount`函数,让语言帮你分发到不同的子类的计算过程中。`volumeCredits`函数的处理也是如法炮制。为此我需要用到多种重构方法,其中最核心的一招是以多态取代条件表达式(272),将多个同样的类型码分支用多态取代。但在施展以多态取代条件表达式(272)之前,我得先创建一个基本的继承结构。我需要先创建一个类,并将价格计算函数和观众量积分计算函数放进去。 我先从检查计算代码开始。(之前的重构带来的一大好处是,现在我大可以忽略那些格式化代码,只要不改变中转数据结构就行。我可以进一步添加测试来保证中转数据结构不会被意外修改。) ##### createStatementData.js... ``` export default function createStatementData(invoice, plays) { const result = {}; result.customer = invoice.customer; result.performances = invoice.performances.map(enrichPerformance); result.totalAmount = totalAmount(result); result.totalVolumeCredits = totalVolumeCredits(result); return result; function enrichPerformance(aPerformance) { const result = Object.assign({}, aPerformance); result.play = playFor(result); result.amount = amountFor(result); result.volumeCredits = volumeCreditsFor(result); return result; } function playFor(aPerformance) { return plays[aPerformance.playID] } function amountFor(aPerformance) { let result = 0; switch (aPerformance.play.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: ${aPerformance.play.type}`); } return result; } function volumeCreditsFor(aPerformance) { let result = 0; result += Math.max(aPerformance.audience - 30, 0); if ("comedy" === aPerformance.play.type) result += Math.floor(aPerformance.audience / 5); return result; } function totalAmount(data) { return data.performances .reduce((total, p) => total + p.amount, 0); } function totalVolumeCredits(data) { return data.performances .reduce((total, p) => total + p.volumeCredits, 0); } ``` ### 创建演出计算器 `enrichPerformance`函数是关键所在,因为正是它用每场演出的数据来填充中转数据结构。目前它直接调用了计算价格和观众量积分的函数,我需要创建一个类,通过这个类来调用这些函数。由于这个类存放了与每场演出相关数据的计算函数,于是我把它称为演出计算器(performance calculator)。 ##### function createStatementData... ``` function enrichPerformance(aPerformance) { const calculator = new PerformanceCalculator(aPerformance); const result = Object.assign({}, aPerformance); result.play = playFor(result); result.amount = amountFor(result); result.volumeCredits = volumeCreditsFor(result); return result; } ``` ##### 顶层作用域... ``` class PerformanceCalculator { constructor(aPerformance) { this.performance = aPerformance; } } ``` 到目前为止,这个新对象还没做什么事。我希望将函数行为搬移进来,这可以从最容易搬移的东西——`play`字段开始。严格来讲,我不需要搬移这个字段,因为它并未体现出多态性,但这样可以把所有数据转换集中到一处地方,保证了代码的一致性和清晰度。 为此,我将使用改变函数声明(124)手法将`performance`的`play`字段传给计算器。 ##### function createStatementData... ``` function enrichPerformance(aPerformance) { const calculator = new PerformanceCalculator(aPerformance, playFor(aPerformance)); const result = Object.assign({}, aPerformance); result.play = calculator.play; result.amount = amountFor(result); result.volumeCredits = volumeCreditsFor(result); return result; } ``` ##### class PerformanceCalculator... ``` class PerformanceCalculator { constructor(aPerformance, aPlay) { this.performance = aPerformance; this.play = aPlay; } } ``` (以下行文中我将不再特别提及“编译、测试、提交”循环,我猜你也已经读得有些厌烦了。但我仍会不断重复这个循环。的确,有时我也会厌烦,直到错误又跳出来咬我一下,我才又学会进入小步的节奏。) ### 将函数搬移进计算器 我要搬移的下一块逻辑,对计算一场演出的价格(amount)来说就尤为重要了。在调整嵌套函数的层级时,我经常将函数挪来挪去,但接下来需要改动到更深入的函数上下文,因此我将小心使用搬移函数(198)来重构它。首先,将`amount`函数的逻辑复制一份到新的上下文中,也就是`PerformanceCalculator`类中。然后微调一下代码,将`aPerformance`改为`this.performance`,将`playFor(aPerformance)`改为`this.play`,使代码适应这个新家。 ##### class PerformanceCalculator... ``` get amount() { let result = 0; switch (this.play.type) { case "tragedy": result = 40000; if (this.performance.audience > 30) { result += 1000 * (this.performance.audience - 30); } break; case "comedy": result = 30000; if (this.performance.audience > 20) { result += 10000 + 500 * (this.performance.audience - 20); } result += 300 * this.performance.audience; break; default: throw new Error(`unknown type: ${this.play.type}`); } return result; } ``` 搬移完成后可以编译一下,看看是否有编译错误。我在本地开发环境运行代码时,编译会自动发生,我实际需要做的只是运行一下Babel。编译能帮我发现新函数中潜在的语法错误,语法之外的就帮不上什么忙了。尽管如此,这一步还是很有用。 使新函数适应新家后,我会将原来的函数改造成一个委托函数,让它直接调用新函数。 ##### function createStatementData... ``` function amountFor(aPerformance) { return new PerformanceCalculator(aPerformance, playFor(aPerformance)).amount; } ``` 现在,我可以执行一次编译、测试、提交,确保代码搬到新家后也能如常工作。之后,我应用内联函数(115),让引用点直接调用新函数(然后编译、测试、提交)。 ##### function createStatementData... ``` function enrichPerformance(aPerformance) { const calculator = new PerformanceCalculator(aPerformance, playFor(aPerformance)); const result = Object.assign({}, aPerformance); result.play = calculator.play; result.amount = calculator.amount; result.volumeCredits = volumeCreditsFor(result); return result; } ``` 搬移观众量积分计算也遵循同样的流程。 ##### function createStatementData... ``` function enrichPerformance(aPerformance) { const calculator = new PerformanceCalculator(aPerformance, playFor(aPerformance)); const result = Object.assign({}, aPerformance); result.play = calculator.play; result.amount = calculator.amount; result.volumeCredits = calculator.volumeCredits; return result; } ``` ##### class PerformanceCalculator... ``` get volumeCredits() { let result = 0; result += Math.max(this.performance.audience - 30, 0); if ("comedy" === this.play.type) result += Math.floor(this.performance.audience / 5); return result; } ``` ### 使演出计算器表现出多态性 我已将全部计算逻辑搬移到一个类中,是时候将它多态化了。第一步是应用以子类取代类型码(362)引入子类,弃用类型代码。为此,我需要为演出计算器创建子类,并在`createStatementData`中获取对应的子类。要得到正确的子类,我需要将构造函数调用替换为一个普通的函数调用,因为JavaScript的构造函数里无法返回子类。于是我使用以工厂函数取代构造函数(334)。 ##### function createStatementData... ``` 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 createPerformanceCalculator(aPerformance, aPlay) { return new PerformanceCalculator(aPerformance, aPlay); } ``` 改造成普通函数后,我就可以在里面创建演出计算器的子类,然后由创建函数决定返回哪一个子类的实例。 ##### 顶层作用域... ``` 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 TragedyCalculator extends PerformanceCalculator { } class ComedyCalculator extends PerformanceCalculator { } ``` 准备好实现多态的类结构后,我就可以继续使用以多态取代条件表达式(272)手法了。 我先从悲剧的价格计算逻辑开始搬移。 ##### class TragedyCalculator... ``` get amount() { let result = 40000; if (this.performance.audience > 30) { result += 1000 * (this.performance.audience - 30); } return result; } ``` 虽说子类有了这个方法已足以覆盖超类对应的条件分支,但要是你也和我一样偏执,你也许还想在超类的分支上抛一个异常。 ##### class PerformanceCalculator... ``` get amount() { let result = 0; switch (this.play.type) { case "tragedy": throw 'bad thing'; case "comedy": result = 30000; if (this.performance.audience > 20) { result += 10000 + 500 * (this.performance.audience - 20); } result += 300 * this.performance.audience; break; default: throw new Error(`unknown type: ${this.play.type}`); } return result; } ``` 虽然我也可以直接删掉处理悲剧的分支,将错误留给默认分支去抛出,但我更喜欢显式地抛出异常——何况这行代码只能再活个几分钟了(这也是我直接抛出一个字符串而不用更好的错误对象的原因)。 再次进行编译、测试、提交。之后,将处理喜剧类型的分支也下移到子类中去。 ##### class ComedyCalculator... ``` get amount() { let result = 30000; if (this.performance.audience > 20) { result += 10000 + 500 * (this.performance.audience - 20); } result += 300 * this.performance.audience; return result; } ``` 理论上讲,我可以将超类的`amount`方法一并移除了,反正它也不应再被调用到。但不删它,给未来的自己留点纪念品也是极好的,顺便可以提醒后来者记得实现这个函数。 ##### class PerformanceCalculator... ``` get amount() { throw new Error('subclass responsibility'); } ``` 下一个要替换的条件表达式是观众量积分的计算。我回顾了一下前面关于未来戏剧类型的讨论,发现大多数剧类在计算积分时都会检查观众数是否达到30,仅一小部分品类有所不同。因此,将更为通用的逻辑放到超类作为默认条件,出现特殊场景时按需覆盖它,听起来十分合理。于是我将一部分喜剧的逻辑下移到子类。 ##### class PerformanceCalculator... ``` get volumeCredits() { return Math.max(this.performance.audience - 30, 0); } ``` ##### class ComedyCalculator... ``` get volumeCredits() { return super.volumeCredits + Math.floor(this.performance.audience / 5); } ```