您的瀏覽器不支援JavaScript功能,若網頁功能無法正常使用時,請開啟瀏覽器JavaScript狀態
Antfire 的生活雜記
Skip
    banner

    Refactoring 讀書心得 part 2

    Refactoring 讀書心得 part 2

    前言

    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 FunctionInline VariableMove FunctionReplace Conditional with Polymorphism等心法進行了重構作業。
    重構作業分成了三個階段,第一,將原本一大段的函式,分解成多個巢狀函式;第二,使用心法Split Phase,將程式碼模組化分成計算、列印兩個部分;第三,將計算器套用了多型,將計算的邏輯由原來的條件式計算移除變成子類別各自計算。

    作者將重構鉅細靡遺地分成多個小步驟,每個小步驟都不影響原來的程式的功能且能通過測試。

    參考文獻

     Comments