1. 程式人生 > 實用技巧 >重構,改善既有程式碼的設計讀後感-拆分計算階段與格式化階段

重構,改善既有程式碼的設計讀後感-拆分計算階段與格式化階段

假如,需要增加一個功能:目前僅僅有文字詳單,需要增加一個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) {
    //...
}

同時,生成中轉引數的函式可以提取到單獨的檔案。
也許,有的程式碼還能夠優化,但是我們經常需要在重構與新增新特性之間尋找平衡。當我們面臨選擇時,應當儘可能的遵循營地法則:保證你離開時的程式碼庫一定比來時更健康。

歡迎大家留言,以便於後面的人更快解決問題!另外亦歡迎大家可以關注我的微信公眾號,方便利用零碎時間互相交流。共勉!