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

    Refactoring 讀書心得 part 1

    Refactoring 讀書心得 part 1

    何謂重構(Refactoring)?

    重構是將一套軟體系統,透過一系列的處理程序,在不改變軟體程式的外在行為的情況下,改動內部架構。
    重構是透過一套有紀律的方法整理程式碼,並且將在變更程式碼架構的過程中,將產生臭蟲(Bugs)的機率降到最低。

    使用範例

    以下將以以下範例著手進行重構

    程式範例

    演出清單.json

    {
     "hamlet": {"name": "Hamlet", "type": "tragedy"},
     "as-like": {"name": "As You Like It", "type": "comedy"},
     "othello": {"name": "Othello", "type": "tragedy"}
    }

    帳單清單.json

    [
     {
      "customer": "BigCo",
      "performances": [
      	{
    	  "playID": "halmet",
    	  "audience": 55
    	},
    	{
    	  "playID": "as-like",
    	  "audience": 35
    	},
    	{
    	  "playID": "othello",
    	  "audience": 40
    	}
      ]
     }
    ]

    列印帳單程式碼

    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`;
      return result;
    }

    以上程式輸出結果為

    State 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

    重構前置作業

    在著手進行任何重構作業前,必須要先確保有一套自我檢測的機制,確保重構後的功能可以正常運作。
    即使依照重構的標準作業心法,亦有可能會產生臭蟲,畢竟人都會犯錯。因此,有一套檢測機制例如單元測試等,可以測試功能的正確性,是一件很重要的事情。

    重構起手式 - 增加程式碼可讀性

    以上面的statement的函式為例,要進行重構像這樣的函式,第一眼看到的是for迴圈中的switch區塊,剛寫完腦袋或許還會記得這一段程式碼的用途,但過了一段時間再回來看,可能需要花一點時間回想一下才想得起來。因此,將這一段程式碼單獨拆出來一個函式,並且用有意義的命名方式為此函式命名amountFor

    分解函式statement

    心法: Extract Function

    function statement (invoice, plays){
      function amountFor(perf, play){
        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}`);
    	}
    	return thisAmount;
      }
      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 = amountFor(perf, play);
    	
    	
    	// 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`;
      return result;
    }

    Refactoring changes the programs in small steps, so if you make a mistake, it is easy to find where the bug is.

    區域變數有意義化

    把函式抽取出來後,接下來整理一下抽取出來後的函式中的區域變數,將其重新命名使它更清楚明瞭,以上面的例子為例,將thisAmount更名為result

    function statement...
      function amountFor(perf, play){
        let result = 0;
    	
    	switch (play.type) {
    	case "tragedy":
    	  result = 40000;
    	  if (perf.audience > 30) {
    	    result += 1000 * (perf.audience - 30);
    	  }
    	  break;
    	case "comedy":
    	  result = 30000;
    	  if (perf.audience > 20) {
    	    result += 10000 + 500 * (perf.audience - 20);
    	  }
    	  result += 300 * perf.audience;
    	  break;
    	default:
    	  throw new Error(`unknown type: ${play.type}`);
    	}
    	return result;
      }
    }

    Any fool can write code that a computer can understand.
    Good programmers write code that humans can understand.

    將第一個參數perf重新命名

    function statement...
      function amountFor(aPerformance, play){
        let result = 0;
    	
    	switch (play.type) {
    	case "tragedy":
    	  result = 40000;
    	  if (perf.audience > 30) {
    	    result += 1000 * (aPerformance.audience - 30);
    	  }
    	  break;
    	case "comedy":
    	  result = 30000;
    	  if (perf.audience > 20) {
    	    result += 10000 + 500 * (aPerformance.audience - 20);
    	  }
    	  result += 300 * aPerformance.audience;
    	  break;
    	default:
    	  throw new Error(`unknown type: ${play.type}`);
    	}
    	return result;
      }
    }

    此命名方式包含了參數的型別及參數名稱 [type][ParameterName],這個命名方式是筆者從Kent Beck學習來的。

    移除play變數

    心法:Replace Temp with Query

    套用心法Replace Temp with Query

    function statement...
      function playFor(aPerformance){
        return plays[aPerformance.playID];
      }
    }

    程式主體變成如下

    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 = playFor(perf);
    	let thisAmount = amountFor(perf, play);
    	
    	
    	// 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`;
      return result;
    }

    接著測試沒問題的話,可以開始消滅變數play

    心法: Inline Variable

    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 = playFor(perf);
    	let thisAmount = amountFor(perf, playFor(perf));
    	
    	
    	// add volume credits
    	volumeCredits += Math.max(perf.audience - 30, 0);
    	// add extra credit for every ten comedy attendees
    	if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5);
    	
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`;
    	totalAmount += thisAmount;
      }
      result += `Amount owed is ${format(totalAmount/100)}\n`;
      result += `You earned ${volumeCredits} credits\n`;
      return result;
    }

    接著是函式amountFor

    function statement...
      function amountFor(aPerformance, play){
        let result = 0;
    	
    	switch (playFor(aPerformance).type) {
    	case "tragedy":
    	  result = 40000;
    	  if (perf.audience > 30) {
    	    result += 1000 * (aPerformance.audience - 30);
    	  }
    	  break;
    	case "comedy":
    	  result = 30000;
    	  if (perf.audience > 20) {
    	    result += 10000 + 500 * (aPerformance.audience - 20);
    	  }
    	  result += 300 * aPerformance.audience;
    	  break;
    	default:
    	  throw new Error(`unknown type: ${playFor(aPerformance).type}`);
    	}
    	return result;
      }
    }

    心法:Change Function Declaration

    因為已經將play套用了心法 Inline Variable
    因此可以套用心法Change Function Declarationplay參數移除

    先修改statement

    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 = playFor(perf);
    	let thisAmount = amountFor(perf);
    	
    	
    	// add volume credits
    	volumeCredits += Math.max(perf.audience - 30, 0);
    	// add extra credit for every ten comedy attendees
    	if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5);
    	
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`;
    	totalAmount += thisAmount;
      }
      result += `Amount owed is ${format(totalAmount/100)}\n`;
      result += `You earned ${volumeCredits} credits\n`;
      return result;
    }

    接著修改amountFor

    function statement...
      function amountFor(aPerformance){
        let result = 0;
    	
    	switch (playFor(aPerformance).type) {
    	case "tragedy":
    	  result = 40000;
    	  if (perf.audience > 30) {
    	    result += 1000 * (aPerformance.audience - 30);
    	  }
    	  break;
    	case "comedy":
    	  result = 30000;
    	  if (perf.audience > 20) {
    	    result += 10000 + 500 * (aPerformance.audience - 20);
    	  }
    	  result += 300 * aPerformance.audience;
    	  break;
    	default:
    	  throw new Error(`unknown type: ${playFor(aPerformance).type}`);
    	}
    	return result;
      }
    }

    接著繼續消除區域變數thisAmount
    套用心法:Inline Variable

    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 = playFor(perf);
    	//let thisAmount = amountFor(perf);
    	
    	// add volume credits
    	volumeCredits += Math.max(perf.audience - 30, 0);
    	// add extra credit for every ten comedy attendees
    	if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5);
    	
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${format(amountFor(perf)/100)} (${perf.audience} seats)\n`;
    	totalAmount += amountFor(perf);
      }
      result += `Amount owed is ${format(amountFor(perf)/100)}\n`;
      result += `You earned ${volumeCredits} credits\n`;
      return result;
    }

    抽出 Volumn Credits變成函式

    function statement...
        function volumeCreditsFor(perf){
          let volumeCredits = 0;
    	  volumeCredits += Math.max(perf.audience - 30, 0);
    	  // add extra credit for every ten comedy attendees
    	  if ("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience / 5);
    	  return volumeCredits;
    	}
    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) {
    	volumeCredits += volumeCreditsFor(perf);
    	
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${format(amountFor(perf)/100)} (${perf.audience} seats)\n`;
    	totalAmount += amountFor(perf);
      }
      result += `Amount owed is ${format(amountFor(perf)/100)}\n`;
      result += `You earned ${volumeCredits} credits\n`;
      return result;
    }

    重新定義參數名稱

    function statement...
        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;
    	}

    移除變數format

    我們先把format的RHS取代成呼叫函式format

    function statement...
        function format(aNumber){
          return new Intl.NumberFormat("en-US",
      						{ style: "currency", currency: "USD",
    						  minimumFractionDigits: 2}).format(aNumber);
    	}

    調整statement

    function statement(invoice, plays) {
      let totalAmount = 0;
      let volumeCredits = 0;
      let result = `Statement for ${invoice.customer}\n`;
    
      for (let perf of invoice.performances) {
    	volumeCredits += volumeCreditsFor(perf);
    	
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${format(amountFor(perf)/100)} (${perf.audience} seats)\n`;
    	totalAmount += amountFor(perf);
      }
      result += `Amount owed is ${format(totalAmount/100)}\n`;
      result += `You earned ${volumeCredits} credits\n`;
      return result;
    }

    調整完後,會發現format在閱讀上不是很直觀,到底是格式化成什麼呢?
    因此,再套用一下心法Change Function Declaration讓它變得更容易閱讀一些。
    變更函式format名稱為usd

    function statement...
        function usd(aNumber){
          return new Intl.NumberFormat("en-US",
    		{ style: "currency", currency: "USD",
    		  minimumFractionDigits: 2}).format(aNumber/100);
    	}
    function statement(invoice, plays) {
      let totalAmount = 0;
      let volumeCredits = 0;
      let result = `Statement for ${invoice.customer}\n`;
    
      for (let perf of invoice.performances) {
    	volumeCredits += volumeCreditsFor(perf);
    	
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
    	totalAmount += amountFor(perf);
      }
      result += `Amount owed is ${usd(totalAmount)}\n`;
      result += `You earned ${volumeCredits} credits\n`;
      return result;
    }

    移除變數volumeCredits

    接下來是移除區域變數volumnCredits,可以看到for迴圈中,做了兩件不同的事情,第一是計算總票數;第二是列印訂單,因此,套用心法:Split Loop,將迴圈裡做的事情拆開。

    function statement(invoice, plays) {
      let totalAmount = 0;
      let volumeCredits = 0;
      let result = `Statement for ${invoice.customer}\n`;
    
      for (let perf of invoice.performances) {
      
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
    	totalAmount += amountFor(perf);
      }
      for (let perf of invoice.performances) {
    	volumeCredits += volumeCreditsFor(perf);
      }
      
      result += `Amount owed is ${usd(totalAmount)}\n`;
      result += `You earned ${volumeCredits} credits\n`;
      return result;
    }

    下一步是套用心法:Slide Statements,將相關的變數宣告,例如:volumeCredits移到for迴圈旁邊。

    function statement(invoice, plays) {
      let totalAmount = 0;
      let result = `Statement for ${invoice.customer}\n`;
    
      for (let perf of invoice.performances) {
      
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
    	totalAmount += amountFor(perf);
      }
      let volumeCredits = 0;  
      for (let perf of invoice.performances) {
    	volumeCredits += volumeCreditsFor(perf);
      }
      
      result += `Amount owed is ${usd(totalAmount)}\n`;
      result += `You earned ${volumeCredits} credits\n`;
      return result;
    }

    再來就是跟之前的步驟一樣,先套用心法:Extract Function再套用心法:Replace Temp with Query
    擷取函式(Extract Function)

    function statement...
        function totalVolumeCredits(){
          let volumeCredits = 0;  
    	  for (let perf of invoice.performances) {
    		volumeCredits += volumeCreditsFor(perf);
    	  }
    	  return volumeCredits;
    	}

    Replace Temp with Query

    function statement(invoice, plays) {
      let totalAmount = 0;
      let result = `Statement for ${invoice.customer}\n`;
    
      for (let perf of invoice.performances) {
      
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
    	totalAmount += amountFor(perf);
      }
      let volumeCredits = totalVolumeCredits();  
      
      result += `Amount owed is ${usd(totalAmount)}\n`;
      result += `You earned ${volumeCredits} credits\n`;
      return result;
    }

    再來套用Inline Variable

    function statement(invoice, plays) {
      let totalAmount = 0;
      let result = `Statement for ${invoice.customer}\n`;
    
      for (let perf of invoice.performances) {
      
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
    	totalAmount += amountFor(perf);
      }
      
      result += `Amount owed is ${usd(totalAmount)}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
    }

    總結以上步驟

    再拆解迴圈的時候,可能會有效能問題,因為原本一個迴圈可以做很多事情,拆完之後變成一個迴圈分別做一件事情。
    作者的建議是先做完重構這件事情之後,再來做效能調教。

    以拆解volumeCredits變數為例,拆解的流程如下

    • Split Loop
    • Slide Statements
    • Extract Function
    • Inline Variable

    接著以同樣的步驟再來移除變數totalAmount

    移除變數totalAmount

    套用心法:Split Loop

    function statement(invoice, plays) {
      let totalAmount = 0;
      let result = `Statement for ${invoice.customer}\n`;
    
      for (let perf of invoice.performances) {
      
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      
      for (let perf of invoice.performances) {
        totalAmount += amountFor(perf);
      }
      result += `Amount owed is ${usd(totalAmount)}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
    }

    套用心法:Slide Statements

    function statement(invoice, plays) {
      let result = `Statement for ${invoice.customer}\n`;
    
      for (let perf of invoice.performances) {
      
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      let totalAmount = 0;
      for (let perf of invoice.performances) {
        totalAmount += amountFor(perf);
      }
      result += `Amount owed is ${usd(totalAmount)}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
    }

    套用心法:Extract Function

    function statement...
        function appleSauce(){
          let totalAmount = 0;
    	  for (let perf of invoice.performances) {
    		totalAmount += amountFor(perf);
    	  }
    	  return totalAmount;
    	}
    function statement(invoice, plays) {
      let result = `Statement for ${invoice.customer}\n`;
    
      for (let perf of invoice.performances) {
      
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      let totalAmount = appleSauce();
      
      result += `Amount owed is ${usd(totalAmount)}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
    }

    套用心法:Inline Variable

    function statement(invoice, plays) {
      let result = `Statement for ${invoice.customer}\n`;
      for (let perf of invoice.performances) {
      
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(appleSauce())}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
    }

    最後是重新命名函式名稱和區域變數名稱

    function statement...
        function totalAmount(){
          let result = 0;
    	  for (let perf of invoice.performances) {
    		result += amountFor(perf);
    	  }
    	  return result;
    	}
    function totalVolumeCredits(){
          let result = 0;  
    	  for (let perf of invoice.performances) {
    		result += volumeCreditsFor(perf);
    	  }
    	  return result;
    	}
    function statement(invoice, plays) {
      let result = `Statement for ${invoice.customer}\n`;
      for (let perf of invoice.performances) {
      
    	// print line for this order
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(totalAmount())}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
    }

    目前重構的狀態

    function statement(invoice, plays) {
      let result = `Statement for ${invoice.customer}\n`;
      for (let perf of invoice.performances) {
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(totalAmount())}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
      
      function totalAmount(){
          let result = 0;
    	  for (let perf of invoice.performances) {
    		result += amountFor(perf);
    	  }
    	  return result;
      }
      
      function totalVolumeCredits(){
          let result = 0;  
    	  for (let perf of invoice.performances) {
    		result += volumeCreditsFor(perf);
    	  }
    	  return result;
      }	
      
      function usd(aNumber){
          return new Intl.NumberFormat("en-US",
    		{ style: "currency", currency: "USD",
    		  minimumFractionDigits: 2}).format(aNumber/100);
      }
      
      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 playFor(aPerformance){
        return plays[aPerformance.playID];
      }
      
      function amountFor(aPerformance){
        let result = 0;
    	switch (playFor(aPerformance).type) {
    	case "tragedy":
    	  result = 40000;
    	  if (perf.audience > 30) {
    	    result += 1000 * (aPerformance.audience - 30);
    	  }
    	  break;
    	case "comedy":
    	  result = 30000;
    	  if (perf.audience > 20) {
    	    result += 10000 + 500 * (aPerformance.audience - 20);
    	  }
    	  result += 300 * aPerformance.audience;
    	  break;
    	default:
    	  throw new Error(`unknown type: ${playFor(aPerformance).type}`);
    	}
    	return result;
      }
    }

    將程式拆成計算及格式化輸出

    在這邊會用到心法:Split Phase來達到將計算跟格式化兩個工作分開

    先套用心法:Extract Function

    function statement (invoice, plays){
      return renderPlainText(invoice, plays);
    }
    function renderPlainText(invoice, plays) {
      let result = `Statement for ${invoice.customer}\n`;
      for (let perf of invoice.performances) {
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(totalAmount())}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
      
      function totalAmount(){...}
      
      function totalVolumeCredits(){...}
      
      function usd(aNumber){...}
      
      function volumeCreditsFor(aPerformance){...}
      
      function playFor(aPerformance){...}
      
      function amountFor(aPerformance){...}
    }

    接著建立一個物件扮演計算及格式化輸出兩個工作的中介資料

    function statement (invoice, plays){
      const statementData = {};
      return renderPlainText(statementData, invoice, plays);
    }
    function renderPlainText(data, invoice, plays) {
      let result = `Statement for ${invoice.customer}\n`;
      for (let perf of invoice.performances) {
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(totalAmount())}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
      
      function totalAmount(){...}
      
      function totalVolumeCredits(){...}
      
      function usd(aNumber){...}
      
      function volumeCreditsFor(aPerformance){...}
      
      function playFor(aPerformance){...}
      
      function amountFor(aPerformance){...}
    }

    下一步是將renderPlainText中會用到的其他參數搬到新增的data物件參數中
    首先是 invoice

    搬移invoice參數到data

    變化過程如下

    function statement (invoice, plays){
      const statementData = {};
      statementData.customer = invoice.customer;
      return renderPlainText(statementData, invoice, plays);
    }
    function renderPlainText(data, invoice, plays) {
      let result = `Statement for ${data.customer}\n`;
      for (let perf of invoice.performances) {
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(totalAmount())}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
      
      function totalAmount(){...}
      
      function totalVolumeCredits(){...}
      
      function usd(aNumber){...}
      
      function volumeCreditsFor(aPerformance){...}
      
      function playFor(aPerformance){...}
      
      function amountFor(aPerformance){...}
    }

    接著

    function statement (invoice, plays){
      const statementData = {};
      statementData.customer = invoice.customer;
      statementData.performances = invoice.performances;
      
      return renderPlainText(statementData, invoice, plays);
    }
    function renderPlainText(data, invoice, plays) {
      let result = `Statement for ${data.customer}\n`;
      for (let perf of data.performances) {
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(totalAmount())}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
      
      function totalAmount(){
          let result = 0;
    	  for (let perf of data.performances) {
    		result += amountFor(perf);
    	  }
    	  return result;
      }
      
      function totalVolumeCredits(){
          let result = 0;  
    	  for (let perf of data.performances) {
    		result += volumeCreditsFor(perf);
    	  }
    	  return result;
      }	
      
      function usd(aNumber){...}
      
      function volumeCreditsFor(aPerformance){...}
      
      function playFor(aPerformance){...}
      
      function amountFor(aPerformance){...}
    }

    確認都搬完了,就可以將參數invoice移除

    function statement (invoice, plays){
      const statementData = {};
      statementData.customer = invoice.customer;
      statementData.performances = invoice.performances;
      
      return renderPlainText(statementData,  plays);
    }
    function renderPlainText(data, plays) {
      let result = `Statement for ${data.customer}\n`;
      for (let perf of data.performances) {
    	result += `  ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(totalAmount())}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
      
      function totalAmount(){
          let result = 0;
    	  for (let perf of data.performances) {
    		result += amountFor(perf);
    	  }
    	  return result;
      }
      
      function totalVolumeCredits(){
          let result = 0;  
    	  for (let perf of data.performances) {
    		result += volumeCreditsFor(perf);
    	  }
    	  return result;
      }	
      
      function usd(aNumber){...}
      
      function volumeCreditsFor(aPerformance){...}
      
      function playFor(aPerformance){...}
      
      function amountFor(aPerformance){...}
    }

    接著將renderPlainText中的playFor(perf).name抽出來,放到中介資料statementDataname屬性

    function statement (invoice, plays){
      const statementData = {};
      statementData.customer = invoice.customer;
      statementData.performances = invoice.performances.map(enrichPerformance);
      
      return renderPlainText(statementData,  plays);
      
      function enrichPerformance(aPerformance) {
        const result = Object.assign({}, aPerformance);
    	return result;
      }
    }
    
    

    因為playFor未來也只有在特定的函式中用到,因此,套用心法:Move Function,將playFor函式移回到statement函式中。

    function statement...
      
      function enrichPerformance(aPerformance) {
        const result = Object.assign({}, aPerformance);
    	result.play = playFor(result);
    	return result;
      }
      
      function playFor(aPerformance){
        return plays[aPerformance.playID];
      }
    }
    
    function renderPlainText...
      let result = `Statement for ${data.customer}\n`;
      for (let perf of data.performances) {
    	result += `  ${perf.play.name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(totalAmount())}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
      
      function volumeCreditsFor(perf){
          let result = 0;
    	  result += Math.max(perf.audience - 30, 0);
    	  // add extra credit for every ten comedy attendees
    	  if ("comedy" === perf.play.type) result += Math.floor(perf.audience / 5);
    	  return result;
      }
      
      function amountFor(aPerformance){
        let result = 0;
    	switch (aPerformance.play.type) {
    	case "tragedy":
    	  result = 40000;
    	  if (perf.audience > 30) {
    	    result += 1000 * (aPerformance.audience - 30);
    	  }
    	  break;
    	case "comedy":
    	  result = 30000;
    	  if (perf.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 statement...
      
      function enrichPerformance(aPerformance) {
        const result = Object.assign({}, aPerformance);
    	result.play = playFor(result);
    	result.amount = amountFor(result);
    	return result;
      }
      
      
      function amountFor(aPerformance){... }
    }
    
    function renderPlainText...
      let result = `Statement for ${data.customer}\n`;
      for (let perf of data.performances) {
    	result += `  ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(totalAmount())}\n`;
      result += `You earned ${totalVolumeCredits()} credits\n`;
      return result;
      function totalAmount(){
          let result = 0;
    	  for (let perf of data.performances) {
    		result += perf.amount;
    	  }
    	  return result;
      }

    還有volumeCreditsFor

    function statement...
      
      function enrichPerformance(aPerformance) {
        const result = Object.assign({}, aPerformance);
    	result.play = playFor(result);
    	result.amount = amountFor(result);
    	result.volumeCredits = volumeCreditsFor(result);
    	return result;
      }
      
      function volumeCreditsFor(aPerformance){...}
    }
    
    
    function renderPlainText...
      
      function totalVolumeCredits(){
          let result = 0;  
    	  for (let perf of data.performances) {
    		result += perf.volumeCredits;
    	  }
    	  return result;
      }	
    }

    確認現階段程式運作正常後,繼續下一步,將函式totalVolumeCredits套用Move Function心法,將函式totalVolumeCredits從函式renderPlainText底下,移回函式statement

    函式totalAmount也是一樣的作法

    function statement (invoice, plays){
      const statementData = {};
      statementData.customer = invoice.customer;
      statementData.performances = invoice.performances.map(enrichPerformance);
      statementData.totalAmount = totalAmount(statementData);
      statementData.totalVolumeCredits = totalVolumeCredits(statementData);
      return renderPlainText(statementData,  plays)
      
      function totalAmount(data){...}
      function totalVolumeCredits(data){...}
    }
    
    function renderPlainText...
      let result = `Statement for ${data.customer}\n`;
      for (let perf of data.performances) {
    	result += `  ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(data.totalAmount)}\n`;
      result += `You earned ${data.totalVolumeCredits} credits\n`;
      return result;

    從函式statement中擷取函式

    function statement (invoice, plays){
      return renderPlainText(createStatementData(statementData,  plays)	);
    }
    function createStatementData (invoice, plays){
      const statementData = {};
      statementData.customer = invoice.customer;
      statementData.performances = invoice.performances.map(enrichPerformance);
      statementData.totalAmount = totalAmount(statementData);
      statementData.totalVolumeCredits = totalVolumeCredits(statementData);
      
      return statementData;
     
    }

    為區域變數重新命名

    function statement (invoice, plays){
      return renderPlainText(createStatementData(statementData,  plays)	);
    }
    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;
     
    }

    套用心法:Replace Loop with Pipeline

    將有使用到迴圈的程式碼,改寫成使用pipeline

    function renderPlainText...
      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);
      }

    目前狀態

    現在已經整理到一個階段了,可以將程式碼分成兩個檔案,一個是創建對帳單(createStatementData.js),一個是呈現對帳單(statement.js)

    function statement (invoice, plays){
      return renderPlainText(createStatementData(statementData,  plays));
    }
    function renderPlainText(data) {
      let result = `Statement for ${data.customer}\n`;
      for (let perf of data.performances) {
    	result += `  ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(data.totalAmount)}\n`;
      result += `You earned ${data.totalVolumeCredits} credits\n`;
      return result;
    }
    function usd(aNumber){
      return new Intl.NumberFormat("en-US",
    	{ style: "currency", currency: "USD",
    	  minimumFractionDigits: 2}).format(aNumber/100);
    }
    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 (perf.audience > 30) {
    	    result += 1000 * (aPerformance.audience - 30);
    	  }
    	  break;
    	case "comedy":
    	  result = 30000;
    	  if (perf.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);
      }	
    }

    拆開後結果如下

    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 (perf.audience > 30) {
    	    result += 1000 * (aPerformance.audience - 30);
    	  }
    	  break;
    	case "comedy":
    	  result = 30000;
    	  if (perf.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);
      }	
    }

    statement.js

    順便新增了 HTML 版本的輸出

    function statement (invoice, plays){
      return renderPlainText(createStatementData(statementData,  plays));
    }
    function renderPlainText(data) {
      let result = `Statement for ${data.customer}\n`;
      for (let perf of data.performances) {
    	result += `  ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)\n`;
      }
      result += `Amount owed is ${usd(data.totalAmount)}\n`;
      result += `You earned ${data.totalVolumeCredits} credits\n`;
      return result;
    }
    
    function htmlStatement (invoice, plays) {
      return renderHtml(createStatementData(invoice, plays));
    }
    
    function renderHtml (data){
      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;
    }
    function usd(aNumber){
      return new Intl.NumberFormat("en-US",
    	{ style: "currency", currency: "USD",
    	  minimumFractionDigits: 2}).format(aNumber/100);
    }

    When programming, follow the camping rule: Always leave the code base healthier than when you found it.

    總結

    藉由Split Loop、Slide Statements、Extract Function、Inline Variable、Move Function、Replace Temp with Query、Split Phase、Replace Loop with Pipeline
    我們將原本耦合度高、程式碼不易閱讀的程式碼,初步變成容易擴充、摸組化的程式碼,並且容易閱讀的兩支js程式。

    接下來,這本書的作者會慢慢把這支程式,轉變成物件導向(Object-oriented)的風格,study-circle part 1 到此結束。

    參考文獻

     Comments