何謂重構(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 Declaration
將play
參數移除
先修改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
抽出來,放到中介資料statementData
的name
屬性
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 到此結束。