欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

重构 新版 - A First Example

程序员文章站 2024-03-21 19:11:46
...

THE STARTING POINT

一家戏剧公司,演员会参加表演。戏剧分为喜剧和悲剧。公司根据观众的规模和戏剧的类型收费。除了账单,还有volume credits,未来的折扣。
plays.json的数据是这样的:

{
    "hamlet": {"name": "Hamlet", "type": "tragedy"},
    "as­like": {"name": "As You Like It", "type": "comedy"},
    "othello": {"name": "Othello", "type": "tragedy"}
}

账单,invoices.json文件是这样的:

[
    {
        "customer": "BigCo",
        "performances": [
            {
                "playID": "hamlet",
                "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);
        totalAmount += thisAmount;
    }
    result += `Amount owed is ${format(totalAmount/100)}\n`;
    result += `You earned ${volumeCredits} credits\n`;
    return result;
}

输出是这样的:

Statement 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

DECOMPOSING THE STATEMENT FUNCTION

先从switch开始重构(Extract Function),其中,把thisAmount重命名为result,把perf重命名为aPerformance。

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

这样,statement修改为:

let thisAmount = amountFor(perf, play);

Removing the play Variable

play可以在amountFor内重新计算。
重构长函数的时候,喜欢摆脱像play这样的变量。因为临时变量会创建很多本地范围内的名称,这些名称使得提取变得负责。这叫Replace Temp with Query。

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

statement修改成:

const play = playFor(perf);

然后,去掉play变量:

for (let perf of invoice.performances) {

    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);
    totalAmount += thisAmount;
}

然后可以Change Function Declaration,删除play参数:

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

我们再看amountFor,它成了一个不再修改的临时变量。于是我们应用Inline Variable。

for (let perf of invoice.performances) {

    // 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);
    totalAmount += amountFor(perf);
}

Extracting Volume Credits

删除play变量以后,通过删除一个本地范围的变量,就容易抽取volume credits计算了。

function volumeCreditsFor(aPerformance) {
    let result = 0;
    result += Math.max(aPerformance.audience ­ 30, 0);
    if ("comedy" === playFor(aPerformance).type) 
        result += Math.floor(aPerformance.audience / 5);
    return result;
}

修改statement:

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);
    totalAmount += amountFor(perf);
}

Removing the format Variable

function format(aNumber) {
    return new Intl.NumberFormat("en­US",
            { style: "currency", currency: "USD",
            minimumFractionDigits: 2 }).format(aNumber);
}

于是:

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);
        totalAmount += amountFor(perf);
    }
    result += `Amount owed is ${format(totalAmount/100)}\n`;
    result += `You earned ${volumeCredits} credits\n`;
    return result;

format没有表达它正在做的事情,formatAsUSD又太长了,所以改成usd。这叫Change Function Declaration。

Removing Total Volume Credits

下一个目标是volumeCredits。它比较棘手,因为它是在循环期间生成的。可以使用Split Loop分开volumeCredits的累加。

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);
        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,把声明挪到下一个循环。

let volumeCredits = 0;
for (let perf of invoice.performances) {
    volumeCredits += volumeCreditsFor(perf);
}

然后,我们抽取函数:

function totalVolumeCredits() {
    let volumeCredits = 0;
    for (let perf of invoice.performances) {
        volumeCredits += volumeCreditsFor(perf);
    }
    return volumeCredits;
}

statement修改为:

result += `Amount owed is ${usd(totalAmount)}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;

然后,删除totalAmount:

function totalAmount() {
    let result = 0;
    for (let perf of invoice.performances) {
        result += amountFor(perf);
    }
    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);
    }
    result += `Amount owed is ${usd(totalAmount())}\n`;
    result += `You earned ${totalVolumeCredits()} credits\n`;
    return result;

STATUS: LOTS OF NESTED FUNCTIONS

现在,我们看一下完整的代码:

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);
    }
    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);
        if ("comedy" === playFor(aPerformance).type)
            result += Math.floor(aPerformance.audience / 5);
        return result;
    }
    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;
    }
}

现在,代码结构好多了。*的statement函数只有几行代码,用来打印。计算逻辑都被移到支持函数。

SPLITTING THE PHASES OF CALCULATION AND FORMATTING

下来,聚焦功能改进-提供HTML版本的statement。
使用Split Phase技术。先把业务逻辑分成两部分,一是计算,二是渲染。
先使用Extract Function,实现Split Phase。

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);
    }
    result += `Amount owed is ${usd(totalAmount())}\n`;
    result += `You earned ${totalVolumeCredits()} credits\n`;
    return result;

其中,statementData对象里包含customer和performances。相应地,也要修改totalAmount()和totalVolumeCredits()函数。

现在,希望play name来自中间数据-使用play来丰富performance。

statementData.customer = invoice.customer;
statementData.performances = invoice.performances.map(enrichPerformance);

return renderPlainText(statementData, plays);

function enrichPerformance(aPerformance) {
    //浅拷贝
    const result = Object.assign({}, aPerformance);
    return result;
}

然后,Move Function:

function enrichPerformance(aPerformance) {
    const result = Object.assign({}, aPerformance);
    result.play = playFor(result);
    return result;
}
function playFor(aPerformance) {
    return plays[aPerformance.playID];
}

这样,renderPlainText函数的playFor(perf).name可以修改为perf.play.name。
同样修改volumeCreditsFor(aPerformance)和amountFor(aPerformance)函数。

再使用同样的办法移动amountFor和volumeCreditsFor。

function enrichPerformance(aPerformance) {
    const result = Object.assign({}, aPerformance);
    result.play = playFor(result);
    result.amountFor = amountFor(result);
    result.volumeCredits = volumeCreditsFor(result);
    return result;
}

这样,statement函数成了:

function statement (invoice, plays) {
    return renderPlainText(createStatementData(invoice, 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;

这样,可以保存成createStatementData.js:

export default function createStatementData(invoice, plays) {
    const result = {};
    result.customer = invoice.customer;
    result.performances = invoice.performances.map(enrichPerformance);result.totalAmount = totalAmount(result);
    result.totalVolumeCredits = totalVolumeCredits(result);
    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 (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.audience ­ 30, 0);
        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);
    }

很容易写HTML版本了:

import createStatementData from './createStatementData.js';
function statement (invoice, plays) {
    return renderPlainText(createStatementData(invoice, plays));
}

function renderPlainText(data, plays) {
    let result = `Statement for ${data.customer}\n`;
    for (let perf of data.performances) {
        result += ` ${perf.play.name}: ${usd(perf.amount)} (${perf.audience} seats)
    }
    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);
}

REORGANIZING THE CALCULATIONS BY TYPE

要支持更多的play类别,每个都有自己的计费和volume credits计算。
对应的策略是Replace Conditional with Polymorphism。

Creating a Performance Calculator

enrichPerformance是关键。

function enrichPerformance(aPerformance) {
    const calculator = new PerformanceCalculator(playFor(aPerformance));
    const result = Object.assign({}, aPerformance);
    result.play = calculator.play;
    result.amount = amountFor(result);
    result.volumeCredits = volumeCreditsFor(result);
    return result;
}

class PerformanceCalculator {
    constructor(aPerformance, aPlay) {
        this.performance = aPerformance;
        this.play = aPlay;
    }
}

Moving Functions into the Calculator

Move Function,把逻辑拷贝到新的上下文。
把aPerformance修改成this.performance,playFor(aPerformance)修改成this.play。

get amount() {
    let result = 0;
    switch (this.play.type) {
        case "tragedy":
            result = 40000;
            if (this.performance.audience > 30) {
                result += 1000 * (this.performance.audience ­ 30);
            }
            break;
        case "comedy":
        result = 30000;
            if (this.performance.audience > 20) {
                result += 10000 + 500 * (this.performance.audience ­ 20);
            }
            result += 300 * this.performance.audience;
            break;
        default:
            throw new Error(`unknown type: ${this.play.type}`);
    }
    return result;
}

function amountFor(aPerformance) {
    return new PerformanceCalculator(aPerformance, playFor(aPerformance)).amount;
}

再使用Inline Function:

result.amount = calculator.amount;

同理:

get volumeCredits() {
    let result = 0;
    result += Math.max(this.performance.audience ­ 30, 0);
    if ("comedy" === this.play.type) 
        result += Math.floor(this.performance.audience / 5);
    return result;
}

result.volumeCredits = calculator.volumeCredits;

Making the Performance Calculator Polymorphic

首先Replace Type Code with Subclasses。
先Replace Constructor with Factory:

const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance));

function createPerformanceCalculator(aPerformance, aPlay) {
    switch(aPlay.type) {
        case "tragedy": return new TragedyCalculator(aPerformance, aPlay);
        case "comedy" : return new ComedyCalculator(aPerformance, aPlay);
        default:throw new Error(`unknown type: ${aPlay.type}`);
    }
}

class TragedyCalculator extends PerformanceCalculator {
}

class ComedyCalculator extends PerformanceCalculator {
}

对于TragedyCalculator:

get amount() {
    let result = 40000;
    if (this.performance.audience > 30) {
        result += 1000 * (this.performance.audience ­ 30);
    }
    return result;
}

对于ComedyCalculator:

get amount() {
    let result = 30000;
    if (this.performance.audience > 20) {
        result += 10000 + 500 * (this.performance.audience ­ 20);
    }
    result += 300 * this.performance.audience;
    return result;
    }

于是,对于PerformanceCalculator:

get amount() {
    throw new Error('subclass responsibility');
}

下一个条件替换是volume credits计算。
大多数plays要检查观众超过30,修改PerformanceCalculator:

get volumeCredits() {
    return Math.max(this.performance.audience ­ 30, 0);
}

对于ComedyCalculator:

get volumeCredits() {
    return super.volumeCredits + Math.floor(this.performance.audience / 5);
}

STATUS: CREATING THE DATA WITH THE POLYMORPHIC CALCULATOR

createStatementData.js最终是:

export default function createStatementData(invoice, plays) {
    const result = {};
    result.customer = invoice.customer;
    result.performances = invoice.performances.map(enrichPerformance);
    result.totalAmount = totalAmount(result);
    result.totalVolumeCredits = totalVolumeCredits(result);
    return result;
    
    function enrichPerformance(aPerformance) {
        const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance));
        const result = Object.assign({}, aPerformance);
        result.play = calculator.play;
        result.amount = calculator.amount;
        result.volumeCredits = calculator.volumeCredits;
        return result;
    }
    function playFor(aPerformance) {
        return plays[aPerformance.playID];
    }
    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);
    }
}

function createPerformanceCalculator(aPerformance, aPlay) {
    switch(aPlay.type) {
        case "tragedy": return new TragedyCalculator(aPerformance, aPlay);
        case "comedy" : return new ComedyCalculator(aPerformance, aPlay);
        default:
            throw new Error(`unknown type: ${aPlay.type}`);
    }
}

class PerformanceCalculator {
    constructor(aPerformance, aPlay) {
        this.performance = aPerformance;
        this.play = aPlay;
    }
    get amount() {
        throw new Error('subclass responsibility');
    }
    get volumeCredits() {
        return Math.max(this.performance.audience ­ 30, 0);
    }
}

class TragedyCalculator extends PerformanceCalculator {
    get amount() {
        let result = 40000;
        if (this.performance.audience > 30) {
            result += 1000 * (this.performance.audience ­ 30);
        }
        return result;
    }
}

class ComedyCalculator extends PerformanceCalculator {
    get amount() {
        let result = 30000;
        if (this.performance.audience > 20) {
            result += 10000 + 500 * (this.performance.audience ­ 20);
        }
        result += 300 * this.performance.audience;
        return result;
    }
    get volumeCredits() {
        return super.volumeCredits + Math.floor(this.performance.audience / 5);
    }
}
相关标签: 重构

上一篇: 文件读写和拷贝

下一篇: