前言
在title-Part 1中,我們從一開始雜亂無章的程式碼著手進行一系列基本的重構流程後,把這段程式碼變成模組化、好擴充的程式碼。
在這篇,我們將使用OOP(Object-Oriented Programming)觀念中的多型(Polymorphism),來針對計算票價的函式來進行重構。
手術開始
目前的createStatementData.js如下
createStatementData.js
function createStatementData (invoice, plays){
const result = {};
result.customer = invoice.customer;
result.performances = invoice.performances.map(enrichPerformance);
result.totalAmount = totalAmount(statementData);
result.totalVolumeCredits = totalVolumeCredits(statementData);
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);
// add extra credit for every ten comedy attendees
if ("comedy" === playFor(aPerformance).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);
}
}
可以看到函式amountFor
裡面有條件式 switch,這邊可以套用新的心法:多型取代條件之術(Replace Conditional with Polymorphism)
不過在進行這段手術之前,需要先建立一個類別存放票價跟票數
Performance Calculator
建立類別首先建立一個類別 PerformanceCalculator
,並且記錄一筆演出資料。
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;
}
top level...
class PerformanceCalculator {
constructor(aPerformance) {
this.performance = aPerformance;
}
}
接下來,會慢慢將原來計算演出資料相關的函式,通通挪到PerformanceCalculator
類別中。
首先整理playFor
,首先套用心法:Change Function Declaration
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;
}
top level...
class PerformanceCalculator {
constructor(aPerformance,aPlay) {
this.performance = aPerformance;
this.play = aPlay;
}
}
接著套用心法:Move Function
將函式amount
搬到PerformanceCalculator
類別中
class PerformanceCalculator {
constructor(aPerformance,aPlay) {
this.performance = aPerformance;
this.play = aPlay;
}
get amount() {
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;
}
}
如此一來,原來的函式amountFor
可以變成如下
function createStatementData...
function amountFor(aPerformance) {
return new PerformanceCalculator(aPerformance, playFor(aPerformance)).amount;
}
enrichPerformance
變成如下
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;
}
result.volumeCredits
的RHS也是一樣的做法
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;
}
類別PerformanceCalculator
新增屬性volumeCredits
class PerformanceCalculator...
get volumeCredits() {
let result = 0;
result += Math.max(this.performance.audience - 30, 0);
// add extra credit for every ten comedy attendees
if ("comedy" === this.play.type) result += Math.floor(this.performance.audience / 5);
return result;
}
}
PerformanceCalculator
多型化
讓第一步先套用心法:Replace Type Code with Subclasses
,此心法主要是根據判斷式回傳對應的子類別,但因為此範例是使用類別,將參數傳入建構子,而且建構子無法回傳值,因此改套用心法:Replace Constructor with Factory Function
,使用一個工廠函式,回傳對應的子類別。
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}`);
}
return new PerformanceCalculator(aPerformance, aPlay);
}
class TragedyCalculator extends PerformanceCalculator {
}
class ComedyCalculator extends PerformanceCalculator {
}
接下來,就可以套用心法:Replace Conditional with Polymorphism
來覆蓋子類別的amount屬性了
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;
}
}
最後將父類別PerformanceCalculator
的屬性amount
改成如下,
class PerformanceCalculator...
get amount() {
throw new Error('subclass responsibility');
}
}
接下來整理一下屬性volumeCredits
,因為只有 Comedy的時候,需要做額外的計算。因此,我們只要在子類別ComedyCalculator
中覆寫該屬性即可。
首先整理父類別PerformanceCalculator
class PerformanceCalculator...
get volumeCredits() {
// let result = 0; //Inline Variable
// result += Math.max(this.performance.audience - 30, 0);
return Math.max(this.performance.audience - 30, 0);;
}
}
class ComedyCalculator...
get volumeCredits() {
return super.volumeCredits + Math.floor(this.performance.audience / 5);
}
目前狀態
createStatementData.js
function createStatementData (invoice, plays){
const result = {};
result.customer = invoice.customer;
result.performances = invoice.performances.map(enrichPerformance);
result.totalAmount = totalAmount(statementData);
result.totalVolumeCredits = totalVolumeCredits(statementData);
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 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);
}
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}`);
}
return new PerformanceCalculator(aPerformance, aPlay);
}
}
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);
}
}
總結
到目前為止,作者用了Extract Function
、Inline Variable
、Move Function
、Replace Conditional with Polymorphism
等心法進行了重構作業。
重構作業分成了三個階段,第一,將原本一大段的函式,分解成多個巢狀函式;第二,使用心法Split Phase
,將程式碼模組化分成計算、列印兩個部分;第三,將計算器套用了多型,將計算的邏輯由原來的條件式計算移除變成子類別各自計算。
作者將重構鉅細靡遺地分成多個小步驟,每個小步驟都不影響原來的程式的功能且能通過測試。