重构,改善既有代码的设计读后感-至大量嵌套函数

2020-12-27  本文已影响0人  东方欲晓_莫道君行早

该书的第一章遥进行重构的代码(补充了一些中文注释):

//剧目数据
var plays =
{
    "hamlet":{"name":"Hamlet","type":"tragedy"},
    "as-like":{"name":"As You Like It","type":"comedy"},
    "othello":{"name":"othello","type":"tragedy"}
}

//账单
var invoices =
[
    {
        "customer":"BigCo", //客户
        "performances":[  //演出情况
            {
                "payID":"hamlet",//莎士比亚的哈姆雷特
                "audience":55 //观众数
            },
            {
                "payID":"as-like",//莎士比亚的皆大欢喜
                "audience":35
            },
            {
                "payID":"othello",//莎士比亚的奥赛罗
                "audience":40
            }
        ]
    }
]


function statement ( invoice,plays ) {
    let totalAmount = 0 ;  // 账单总额
    let volumeCredits = 0 ;  //观众量积分,用于获取折扣,提升客户忠诚度
    let result = `Statement for ${invoice.customer}\n`; //用于打印的字符串

    //用于格式化数字,优化显示货币。
    //locale 是必传参数(支持不同国家的数字分隔符格式)
    //option 是可选参数是一个对象
    //style,currency 可以设置货币符号或者精确度计算
    //unit 是可选参数,可以增加单位(长度单位等)
    const format = new Intl.NumberFormat("en-US",{ style:"currency",currency:"USD",minimumFractionDigits:2}).format;
    for(let perf of invoice.performances){
        const play = plays[perf.payID];
        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); //取两者较大数,防止结果小于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;
}

通过调用函数statement ( invoices[0],plays )便可以打印出结果

statement.png

如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于修改,那就先重构那个程序使其比较容易添加该特性,然后再添加该特性。

重构前,先检查自己是否有一套可靠的测试集。这些测试,必须有自我检验能力。

本书第一章举了一个例子,将一个函数进行拆分重构。以我不太充分的知识库看来,主要做的就是:
1.将函数中独立的功能重新写成函数,在顶层函数调用。
2.对顶层函数与拆分出来的函数进行变量名优化。
3.将一些临时变量提炼成为函数,通过直接调用函数来获取。
4.将一些在循环中累加的变量,将其剥离出来,在函数中重新遍历累加,以实现以查询取代临时变量的目的。

有两个问题:
1.提炼临时变量,会造成多次调用函数
2.同一个循环拆成多个同样会造成性能损耗

书中写道:

大多数时候,重复一次这样的循环对性能的影响都可以忽略不计。
当然,’大多数时候‘不等同于‘所有时候’。有时,一些重构手法会显著的影响性能。但即便如此,我也不去管他,继续重构,因为有了一份结构良好的代码,回头调优其性能也会容易得多。如果我在重构时引入了明显的性能消耗,我后面会花时间进行性能调优。进行调优时,可能会回退我早先做的一些重构----但更多时候,因为重构,我可以使用更高效的调优方案。最后我得到的是既整洁又高效的代码。

但是针对实际的工作环境,在特定时间长度下,能做的工作有限。所以个人觉得,并非所有重构都要按部就班,可以根据实际情况具体分析。如果时间不够,对于一些可能会影响性能的暂时忽略。如果时间允许,则可以尝试,先重构,后调优的路子。---仅个人见解

编程时,需要遵循营地法则:保证你离开时的代码库一定比来时更健康。

实际编程中,普通程序员实现可能比较难。因为在一定的时间内,除了要完成任务之外还要重构会耽误文本的开发计划。而且重构并不能保证代码不会出现bug。程序很脆弱,每一次改动都应当有比较完备的测试。
实际上,可以针对一些方法进行简单重构,过程也不一定时持续的,也不一定是一个人。
更好的时有领导决定,以任务的形式派发下来,给重构代码的程序员时间。

重构技术,就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现他。

实际工作中,可能不需要这样,如果仅仅是 如修改参数变量名这样的事情。当然了,如果步伐更好,出错概率也更小,好处是显而易见的。这是在测试容易的情况下。如果测试过程比较耗时,就需要你自己平衡关系了。

跟随书中示例代码,大概的见识一下重构的过程。
第一眼比较容易的看到的时swich语句,这段代码用来计算每一种剧目需要的花费,所以可以考虑将这段抽取成独立的函数。这个过程称为 提炼函数(106).
抽取的代码 涉及三个临时变量,其中perf,play仅仅是使用,不会修改,可以作为参数传过来,thisAmount是改变的,可以将其作为返回值。同时为了更明显的看出作为返回值的变量,故修改变量名。参数命名也同样需要让其意义更确定。以定冠词开头,意义更明显。当然,如果可以的话,修改临时变量与参数应该分成单独的步骤。结果如下:

//提炼函数
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 aPerformance of invoice.performances){
        const play = plays[aPerformance.payID];
        let thisAmount = amountFor(aPerformance,play) ;

        //计算观众积分 add volume credits
        volumeCredits += Math.max(aPerformance.audience - 30,0); //取两者较大数,防止结果小于0
        //add extra credit for every ten comedy attendees
        if("comedy" === play.type)  volumeCredits += Math.floor(aPerformance.audience / 5);

        //print line for this order
        result += ` ${play.name}:${format(thisAmount/100)} (${aPerformance.audience} seats)\n`;
        totalAmount += thisAmount;
    }
    result += `Amount owed is ${format(totalAmount/100)}\n`;
    result += `You earned ${volumeCredits} credits\n`;
    return result;

    function amountFor(aPerformance,play){
        let result= 0 ;
        //用于计算总账单
        switch (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:${play.type}`);
        }
        return result;
    }
}

最上层函数看起来明晰了很多。

以查询取代临时变量(178)
观察提炼出来的amountFor函数的参数,可以发现第一个参数是从循环中取得,每次都是变化的,但是第二个参数是由performances计算得来,因此没必要将其作为参数引入。

//内联变量
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 aPerformance of invoice.performances){
        let thisAmount = amountFor(aPerformance,playFor(aPerformance)) ;

        //计算观众积分 add volume credits
        volumeCredits += Math.max(aPerformance.audience - 30,0); //取两者较大数,防止结果小于0
        //add extra credit for every ten comedy attendees
        if("comedy" === playFor(aPerformance).type)  volumeCredits += Math.floor(aPerformance.audience / 5);

        //print line for this order
        result += ` ${playFor(aPerformance).name}:${format(thisAmount/100)} (${aPerformance.audience} seats)\n`;
        totalAmount += thisAmount;
    }
    result += `Amount owed is ${format(totalAmount/100)}\n`;
    result += `You earned ${volumeCredits} credits\n`;
    return result;

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

    function amountFor(aPerformance,play){
        let result= 0 ;
        //用于计算总账单
        switch (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:${play.type}`);
        }
        return result;
    }
}

改变函数声明(124)

//改变函数声明(124)
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 aPerformance of invoice.performances){
        let thisAmount = amountFor(aPerformance) ;

        //计算观众积分 add volume credits
        volumeCredits += Math.max(aPerformance.audience - 30,0); //取两者较大数,防止结果小于0
        //add extra credit for every ten comedy attendees
        if("comedy" === playFor(aPerformance).type)  volumeCredits += Math.floor(aPerformance.audience / 5);

        //print line for this order
        result += ` ${playFor(aPerformance).name}:${format(thisAmount/100)} (${aPerformance.audience} seats)\n`;
        totalAmount += thisAmount;
    }
    result += `Amount owed is ${format(totalAmount/100)}\n`;
    result += `You earned ${volumeCredits} credits\n`;
    return result;

    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;
    }
}

提炼计算观众积分的逻辑
将函数变量改成函数声明,同时注意函数名要尽量表意。

//改变函数声明(124)
function statement ( invoice,plays ) {
    let totalAmount = 0 ;  // 账单总额
    let volumeCredits = 0 ;  //观众量积分,用于获取折扣,提升客户忠诚度
    let result = `Statement for ${invoice.customer}\n`; //用于打印的字符串
    for(let aPerformance of invoice.performances){
        let thisAmount = amountFor(aPerformance) ;
        //计算观众积分 add volume credits
        volumeCredits += volumeCreditsFor(aPerformance);
        //print line for this order
        result += ` ${playFor(aPerformance).name}:${usd(thisAmount/100)} (${aPerformance.audience} seats)\n`;
        totalAmount += thisAmount;
    }
    result += `Amount owed is ${usd(totalAmount/100)}\n`;
    result += `You earned ${volumeCredits} credits\n`;
    return result;

    //这一轮循环增加的量
    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;
    }
}

移除观众量总积分

//移除观众量积分总和
function statement ( invoice,plays ) {
    let totalAmount = 0 ;  // 账单总额
    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`;
        totalAmount += amountFor(aPerformance);
    }
    let volumeCredits = totalVolumeCredits();//计算观众积分 add volume credits
    result += `Amount owed is ${usd(totalAmount/100)}\n`;
    result += `You earned ${volumeCredits} credits\n`;
    return result;

    //计算观众积分 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;
    }
}

其中使用了
拆分循环(227) 分离出累加过程
移动语句(223) 将累加变量的声明与累加过程集中到一起
提炼函数(106) 提炼出计算总和的函数
内联变量(123) 完全移除中间变量

同样的步骤来移除totalAmount

//移除观众量积分总和
function statement ( 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;
    }
}

将函数折叠后再看,是不是思路比较清晰明了了


daima.png
上一篇下一篇

猜你喜欢

热点阅读