重构,改善既有代码的设计读后感-拆分计算阶段与格式化阶段

假如,需要增加一个功能:目前仅仅有文本详单,需要增加一个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) {
    //...
}

同时,生成中转参数的函数可以提取到单独的文件。
也许,有的代码还能够优化,但是我们经常需要在重构与添加新特性之间寻找平衡。当我们面临选择时,应当尽可能的遵循营地法则:保证你离开时的代码库一定比来时更健康。

欢迎大家留言,以便于后面的人更快解决问题!另外亦欢迎大家可以关注我的微信公众号,方便利用零碎时间互相交流。共勉!

posted @ 2020-12-31 18:42  东方欲晓_莫道君行早  阅读(132)  评论(0编辑  收藏  举报