重構,改善既有程式碼的設計讀後感-拆分計算階段與格式化階段
假如,需要增加一個功能:目前僅僅有文字詳單,需要增加一個HTML詳單。如果僅僅是重構到當前步驟,還需要將函式拷貝到另一個函式中。雖然,條例也算清晰,但是如果我們實現的更好,能將所有需要的資料放到一個數據結構,HTML詳單可以呼叫一個函式獲取所有資料,在進行HTML編碼,效果會更好吧。
拆分階段(154)
要實現服用由多種方法,本書推薦的技術是拆分階段。本例中第一階段為產生資料,第二階段為渲染資料。要開始拆分階段,應該先對第二階段的程式碼應用提煉函式。在這個例子中,這部分程式碼就是列印詳單的程式碼,即statement函式的全部內容(七行程式碼)。要把他們與所有巢狀函式一起抽象到一個新的頂層函式中。然後建立一個物件,作為兩個階段間傳遞的中轉資料結構。
function statement(invoice,plays) { const statementData = {}; return renderPlainText(statementData,invoice,plays);//第二階段 } //拆分階段 render 呈現 plaintext純文字 function renderPlainText (data, invoice,plays ) { let result = `Statement for ${invoice.customer}\n`; //用於列印的字串 for(let aPerformance of invoice.performances){ //print line for this order result += ` ${playFor(aPerformance).name}:${usd(amountFor(aPerformance)/100)} (${aPerformance.audience} seats)\n`; } result += `Amount owed is ${usd(totalAmount()/100)}\n`; result += `You earned ${totalVolumeCredits()} credits\n`; return result; //計算總的賬目 function totalAmount() { let totalAmount = 0 ; // 賬單總額 for(let aPerformance of invoice.performances){ totalAmount += amountFor(aPerformance); } return totalAmount; } //計算觀眾積分 add volume credits function totalVolumeCredits() { let volumeCredits = 0 ; //觀眾量積分,用於獲取折扣,提升客戶忠誠度 for(let aPerformance of invoice.performances){ //計算觀眾積分 add volume credits volumeCredits += volumeCreditsFor(aPerformance); } return volumeCredits; } //這一輪迴圈增加的量 function volumeCreditsFor(aPerformance) { let result = 0; result += Math.max(aPerformance - 30 , 0); if ("comedy" == playFor(aPerformance).type) result += Math.floor(aPerformance.audience / 5); return result; } //格式化數字,顯示為貨幣 function usd(aNumber) { return new Intl.NumberFormat("en-US", { style:"currency",currency:"USD", minimumFractionDigits:2}).format(aNumber/100); } //獲取某一劇目 function playFor(aPerformance) { return plays[aPerformance.playID]; } //計算某一劇目需要的賬目 function amountFor(aPerformance){ let result= 0 ; //用於計算總賬單 switch (playFor(aPerformance).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:${playFor(aPerformance).type}`); } return result; } }
現在檢查一下renderPlaintext其他引數,我希望將引數都挪到中轉引數中,讓renderPlainText只操作data傳過來的資料。
那麼就先invoice的兩個屬性到data
function statement(invoice,plays) { const statementData = {}; statementData.customer = invoice.customer; statementData.performances = invoice.performances; return renderPlainText(statementData,plays); } //拆分階段 render 呈現 plaintext純文字 function renderPlainText (data,plays ) { let result = `Statement for ${data.customer}\n`; //用於列印的字串 for(let aPerformance of data.performances){ //print line for this order result += ` ${playFor(aPerformance).name}:${usd(amountFor(aPerformance)/100)} (${aPerformance.audience} seats)\n`; } result += `Amount owed is ${usd(totalAmount()/100)}\n`; result += `You earned ${totalVolumeCredits()} credits\n`; return result; //計算總的賬目 function totalAmount() { let totalAmount = 0 ; // 賬單總額 for(let aPerformance of data.performances){ totalAmount += amountFor(aPerformance); } return totalAmount; } //計算觀眾積分 add volume credits function totalVolumeCredits() { let volumeCredits = 0 ; //觀眾量積分,用於獲取折扣,提升客戶忠誠度 for(let aPerformance of data.performances){ //計算觀眾積分 add volume credits volumeCredits += volumeCreditsFor(aPerformance); } return volumeCredits; } //這一輪迴圈增加的量 function volumeCreditsFor(aPerformance) { let result = 0; result += Math.max(aPerformance - 30 , 0); if ("comedy" == playFor(aPerformance).type) result += Math.floor(aPerformance.audience / 5); return result; } //格式化數字,顯示為貨幣 function usd(aNumber) { return new Intl.NumberFormat("en-US", { style:"currency",currency:"USD", minimumFractionDigits:2}).format(aNumber/100); } //獲取某一劇目 function playFor(aPerformance) { return plays[aPerformance.playID]; } //計算某一劇目需要的賬目 function amountFor(aPerformance){ let result= 0 ; //用於計算總賬單 switch (playFor(aPerformance).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:${playFor(aPerformance).type}`); } return result; } }
另外,如果希望“劇目名稱”資料也從中轉資料得來,就需要使用play中的資料填充aPerformance物件。
同樣的手法處理amountFor
接下來就搬移觀眾量積分,然後將兩個計算總數的搬移到statement函式中,同時將usd移動到頂層,以便於 其他渲染方式呼叫,結果如下:
function statement(invoice,plays) {
const statementData = {};
statementData.customer = invoice.customer;
//這裡是一個知識點,類似與Java的新迴圈
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
return renderPlainText(statementData,plays);
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 - 30 , 0);
if ("comedy" == aPerformance.play.type) result += Math.floor(aPerformance.audience / 5);
return result;
}
//計算總的賬目
function totalAmount(data) {
let totalAmount = 0 ; // 賬單總額
for(let aPerformance of data.performances){
totalAmount += aPerformance.amount;
}
return totalAmount;
}
//計算觀眾積分 add volume credits
function totalVolumeCredits(data) {
let volumeCredits = 0 ; //觀眾量積分,用於獲取折扣,提升客戶忠誠度
for(let aPerformance of data.performances){
//計算觀眾積分 add volume credits
volumeCredits += aPerformance.volumeCredits;
}
return volumeCredits;
}
}
//拆分階段 render 呈現 plaintext純文字
function renderPlainText (data,plays ) {
let result = `Statement for ${data.customer}\n`; //用於列印的字串
for(let aPerformance of data.performances){
//print line for this order
result += ` ${aPerformance.play.name}:${usd(aPerformance.amount/100)} (${aPerformance.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);
}
以管道取代迴圈(231)
接下來以管道取代迴圈(231),就可以將第一階段的程式碼提取到獨立的函式中了。同時,再去實現html版本就非常容易了,也很好的實現了程式碼的複用。
function statement() {
return renderPlainText(createStatementData(invoices,plays))
}
function createStatementData(invoice,plays) {
const statementData = {};
statementData.customer = invoice.customer;
//這裡是一個知識點,類似與Java的新迴圈
statementData.performances = invoice.performances.map(enrichPerformance);
statementData.totalAmount = totalAmount(statementData);
statementData.totalVolumeCredits = totalVolumeCredits(statementData);
return statementData;
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 - 30 , 0);
if ("comedy" == aPerformance.play.type) result += Math.floor(aPerformance.audience / 5);
return result;
}
//計算總的賬目
function totalAmount(data) {
return data.performances.reduce((total,p) =>total+p.amount,0);
}
//計算觀眾積分 add volume credits
function totalVolumeCredits(data) {
return data.performances.reduce((total,p) =>total+p.volumeCredits,0)
}
}
//拆分階段 render 呈現 plaintext純文字
function renderPlainText (data,plays ) {
let result = `Statement for ${data.customer}\n`; //用於列印的字串
for(let aPerformance of data.performances){
//print line for this order
result += ` ${aPerformance.play.name}:${usd(aPerformance.amount/100)} (${aPerformance.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 htmlStatement(invoices,plays) {
return rederHtml(createStatementData(invoices,plays));
}
function rederHtml(data) {
//...
}
同時,生成中轉引數的函式可以提取到單獨的檔案。
也許,有的程式碼還能夠優化,但是我們經常需要在重構與新增新特性之間尋找平衡。當我們面臨選擇時,應當儘可能的遵循營地法則:保證你離開時的程式碼庫一定比來時更健康。