重构--第一个案例

1.1 起点

实例非常简单。这是一个影片出租店的程序,计算每一位顾客的消费金额并打印详单。操作者告诉程序:顾客租了哪些影片、租期多长,程序便根据租赁时间和影片类型算出费用。影片分为三类:普通片、儿童片和新片。除了计算费用,还要为常客计算积分,积分会根据租片种类是否为新片而有所不同。

我会逐一列出这些类的代码。

Movie(影片)

public class Movie {
    public static final int CHILDRENS = 2;
    public static final int REGULAR = 0;
    public static final int NEW_RELEASE = 1;

    private String _title;
    private int _priceCode;

    public Movie(String title, int priceCode) {
        _title = title;
        _priceCode = priceCode;
    }

    public String get_title() {
        return _title;
    }

    public void set_title(String _title) {
        this._title = _title;
    }

    public int get_priceCode() {
        return _priceCode;
    }

    public void set_priceCode(int _priceCode) {
        this._priceCode = _priceCode;
    }
}

Rental(租赁)

public class Rental {
    private Movie _movie;
    private int _daysRented;

    public Rental(Movie movie, int daysRented) {
        _movie = movie;
        _daysRented = daysRented;
    }

    public int getDaysRented() {
        return _daysRented;
    }

    public Movie getMovie() {
        return _movie;
    }
}

Customer(顾客)

public class Customer {
    private String _name;
    private Vector _rentals = new Vector();

    public Customer(String name) {
        _name = name;
    }

    public void addRental(Rental arg) {
        _rentals.addElement(arg);
    }

    public String getName() {
        return _name;
    }
}

Customer还提供了一个用于生成详单的函数,完整代码如下:

public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            double thisAmount = 0;
            Rental each = (Rental) rentals.nextElement();

            switch (each.getMovie().get_priceCode()) {
                case Movie.REGULAR:
                    thisAmount += 2;
                    if (each.getDaysRented() > 2) {
                        thisAmount += (each.getDaysRented() - 2) * 1.5;
                    }
                    break;
                case Movie.CHILDRENS:
                    thisAmount += each.getDaysRented() * 3;
                    break;
                case Movie.NEW_RELEASE:
                    thisAmount += 1.5;
                    if (each.getDaysRented() > 3) {
                        thisAmount += (each.getDaysRented() - 3) * 1.5;
                    }
                    break;
                default:
                    break;
            }

            // add grequent renter points
            frequentRenterPoints++;
            // add bonus for a two day new release rental
            if ((each.getMovie().get_priceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) {
                frequentRenterPoints++;
            }

            // show fingures for this rental
            result += "\t" + each.getMovie().get_title() + "\t" + String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;
        }

        // add footer lines
        result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
        return result;
    }

对此起始程序的评价

这个起始程序给你留下了什么印象?我会说它设计的不好,而且很明显不符合面向对象精神。对于这样一个小程序,这些缺点其实没有什么大不了的。快速而随性的设计一个简单的程序并没有错。但如果这是复杂系统中具有代表性的一段,那么我就真的要对这个程序信心动摇了。Customer里头那个长长的statement()做的事情太多了,它做了很多原本应该由其他类完成的事情。

即便如此,这个程序还是能正常工作。座椅这只是美学意义上的判断,只是对丑陋代码的厌恶,是吗?如果不去修改这个系统,那么的确如此,编译才不会在乎代码好不好看呢。但是当我们打算修改系统的时候,就涉及到了人,而人在乎这些。差劲的系统是很难修改的,因为很难找到修改点。如果很难找到修改点,程序员就很有可能犯错,从而引入bug。

在这个例子里,我们的用户希望对系统做一点修改。首先他们希望以HTML格式输出详单,这样就可以直接在网页上显示,这非常符合时下的潮流。现在请你想一想,这个变化会带来什么影响。看看代码你就会发现,根本不可能在打印HTML报表的函数中复用目前statement()的任何行为。你唯一可以做的就是编写一个全新的htmlStatement(),大量重复statement()的行为。当然,现在做这个还不太费力,你可以把statement复制一份然后按需要修改就是了。

但如果计费标准发生变化,又会如何?你必须同时修改statement和htmlStatement,并确保两处修改的一致性。当你后续还要再修改时,复制黏贴带来的问题就浮现出来了。如果你编写的是一个用不需要修改的程序,难么剪剪贴贴还好,但如果程序要保存很长时间,而且可能需要修改,复制黏贴行为就会造成潜在的威胁。

现在第二个变化来了:用户希望改变影片的分类规则,但是还没有决定怎么改。他们设想了集中方案,这些方案都会影响顾客消费和常客积分点的计算方式。座位一个经验丰富的开发者,你可以肯定:不论用户提出什么方案,你唯一能够获得保证就是他们一定会在六个月之内再次修改它。

为了应付分类规则和计费规则的变化,程序必须对statement作出修改。但是如果我们把statement内的代码复制到用以打印Html详单的函数中,就必须确保将来的任何修改在两个地方保持一致。随着各种规则变得越来越复杂,适当的修改点越来越难找,不犯错的机会也越来越少。

你的态度也许倾向于尽量少修改程序:不管怎么说,他还运行的很好,你心里牢牢记着那句古老的工程谚语“如果他没坏,就不要动它”这个程序也许还没坏掉,但他造成了上海。它让你的生活比较难过,因为你发现很难完成客户所需的修改。这时候,重构技术就该粉墨登场了。

1.2 重构的第一步

每当我要进行重构的时候,第一个步骤永远相同:我得为即将修改的代码建立一组可靠的测试环境。这些测试是必要的,因为尽管遵循重构手法可以使我避免大多数引入bug的情形,但我毕竟是人,毕竟有可能犯错。所以我需要可靠的测试。

1.3 分解并重组statement()

第一个明显引起我注意的就是长得离谱的statement()。每当看到这样长长的函数,我就想把它大卸八块。要知道,代码块越小,代码的功能就越容易管理,代码的处理和移动也就越轻松。

本章重构过程的第一阶段中,我将说明如何把长长的函数切开,并把较小块的代码移至更合适的类。我希望降低代码重复量,从而使新的函数(打印HTML详单用的)更容易编写。

第一个步骤是找出代码的逻辑泥团并运用Extract Method。本例一个明显的逻辑泥团就是switch语句,把它提炼到独立函数中似乎比较好。

首先我得在这段代码里找出函数内的局部变量和参数。我找到了两个,each和thisAmount,前者并未被修改,候着会被修改。任何不会被修改的变量都可以被我当成参数传入新的函数,至于会被修改的变量就需格外小心,如果只有一个变量会被修改,我可以把它当做返回值。thisAmount是个临时变量,其值在每次循环起始处被设为0,并且switch语句之前不会改变,所以我可以直接把新函数的返回值赋给他。

下面展示了重构后的代码:

public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            double thisAmount = 0;
            Rental each = (Rental) rentals.nextElement();

            thisAmount = amountDor(each);

            // add grequent renter points
            frequentRenterPoints++;
            // add bonus for a two day new release rental
            if ((each.getMovie().get_priceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) {
                frequentRenterPoints++;
            }

            // show fingures for this rental
            result += "\t" + each.getMovie().get_title() + "\t" + String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;
        }

        // add footer lines
        result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
        return result;
    }

    private double amountDor(Rental each) {
        int thisAmount = 0;
        switch (each.getMovie().get_priceCode()) {
            case Movie.REGULAR:
                thisAmount += 2;
                if (each.getDaysRented() > 2) {
                    thisAmount += (each.getDaysRented() - 2) * 1.5;
                }
                break;
            case Movie.CHILDRENS:
                thisAmount += each.getDaysRented() * 3;
                break;
            case Movie.NEW_RELEASE:
                thisAmount += 1.5;
                if (each.getDaysRented() > 3) {
                    thisAmount += (each.getDaysRented() - 3) * 1.5;
                }
                break;
            default:
                break;
        }
        return thisAmount;
    }

现在我已经把原来的函数分为两块,可以分别处理他们。我不喜欢amountFor内的某些变量名称,现在正是修改他们的时候。

private double amountDor(Rental aRental) {

double result = 0;

switch (aRental.getMovie().get_priceCode()) {

case Movie.REGULAR:

result += 2;

if (aRental.getDaysRented() > 2) {

result += (aRental.getDaysRented() - 2) * 1.5;

}

break;

case Movie.CHILDRENS:

result += aRental.getDaysRented() * 3;

break;

case Movie.NEW_RELEASE:

result += 1.5;

if (aRental.getDaysRented() > 3) {

result += (aRental.getDaysRented() - 3) * 1.5;

}

break;

default:

break;

}

return result;

}

改变变量名称是值得的行为吗?绝对值得。好的代码应该清楚表达出自己的功能,变量名称是代码清晰的关键。任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码才是优秀的程序员。

代码应该表现自己的目的,这一点非常重要。

搬移“金额计算”代码

观察amountFor时,我发现这个函数使用了来自Rental类的信息,却没有使用来自Customer类的信息。这立刻使我怀疑它是否被放错了位置。绝大多数情况下,函数应该放在它所使用的数据的所属对象内,所以amountFor应该移到Rental类去

class Rental...
public double getCharge() {
        double result = 0;
        switch (getMovie().get_priceCode()) {
            case Movie.REGULAR:
                result += 2;
                if (getDaysRented() > 2) {
                    result += (getDaysRented() - 2) * 1.5;
                }
                break;
            case Movie.CHILDRENS:
                result += getDaysRented() * 3;
                break;
            case Movie.NEW_RELEASE:
                result += 1.5;
                if (getDaysRented() > 3) {
                    result += (getDaysRented() - 3) * 1.5;
                }
                break;
            default:
                break;
        }
        return result;
    }

class Customer...
private double amountDor(Rental aRental) {
        return aRental.getCharge();
    }

下一个步骤是找出程序中对于就函数的所有引用点,并修改他们,让他们改用新函数,本例中,这个步骤很简单,因为只有一个地方使用它,一般情况下,你得在可能运用该函数的所有类中查找一遍。

...
thisAmount = each.getCharge();
...

这时候,引起我注意的是:thisAmount如今变得多余了。它接受each.getRecharge()的执行结果,然后就不再有任何改变。所以我可以把thisAmount除去:

public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            // add grequent renter points
            frequentRenterPoints++;
            // add bonus for a two day new release rental
            if ((each.getMovie().get_priceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) {
                frequentRenterPoints++;
            }

            // show fingures for this rental
            result += "\t" + each.getMovie().get_title() + "\t" + String.valueOf(each.getCharge()) + "\n";
            totalAmount += each.getCharge();
        }

        // add footer lines
        result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
        return result;
    }

我喜欢尽量除去这一类临时变量。临时变量往往引发问题,他们会导致大量参数被传来传去,而其实完全没有这种必要。你很容易跟丢他们,尤其在长长的函数之中更是如此。当然我这么做也需付出性能上的代价,例如本例中的费用就被计算了两次。但是这很容易在Rental类中被优化。而且如果代码有合理的组织和管理,优化就会有很好的效果。

提炼“常客积分计算”代码

下一步要对“常客积分计算”做类似的湖里。积分的计算视影片种类而有不同,不过不像收费规则有那么多变化。看来似乎有理由把积分计算责任放在Rental类身上。

我们先来看看局部变量。这里再一次用到了each,而它可以被当做参数传入新函数中。另一个临时变量是frequentRenterPoints。本例中的它在被使用之前已经先有初值,但提炼出来的函数并没有读取该值,所以我们不需要将它当做参数穿进去,只需把新函数的返回值累加上去就行了。

class Customer...
public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();

            frequentRenterPoints += each.getFrequentRenterPoints();

            // show fingures for this rental
            result += "\t" + each.getMovie().get_title() + "\t" + String.valueOf(each.getCharge()) + "\n";
            totalAmount += each.getCharge();
        }

        // add footer lines
        result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
        return result;
    }

class Rental
public int getFrequentRenterPoints() {
        if ((getMovie().get_priceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1)
            return 2;
        else
            return 1;
    }

去除临时变量

正如我们前面所提过的,临时变量可能是个问题。他们只在自己所属的函数中奏效,所以他们会助长冗长而复杂的函数。这里有两个临时变量,两者都是用来从Customer对象相关的Rental对象中获得某个总量。我打算运用查询函数来取代totalAmount和frequentRentalPoints这两个临时变量。由于类中任何函数都可以调用上述查询函数,所以它能够促成较干净的设计,而减少冗长复杂的函数。

首先,我用Customer类的getTotalRecharge()来取代totalAmount

public String statement() {
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();

            frequentRenterPoints += each.getFrequentRenterPoints();

            // show fingures for this rental
            result += "\t" + each.getMovie().get_title() + "\t" + String.valueOf(each.getCharge()) + "\n";
        }

        // add footer lines
        result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
        return result;
    }

    private double getTotalCharge() {
        double result = 0;
        Enumeration rentals = _rentals.elements();
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            result += each.getCharge();
        }
        return result;
    }

由于totalAmount在循环内部被赋值,我不得不把循环复制到查询函数中。

然后以同样的手法处理frequentRenterPoints:

public String statement() {
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            // show fingures for this rental
            result += "\t" + each.getMovie().get_title() + "\t" + String.valueOf(each.getCharge()) + "\n";
        }

        // add footer lines
        result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
        result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points";
        return result;
    }

private int getTotalFrequentRenterPoints() {
        int result = 0;
        Enumeration rentals = _rentals.elements();
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            result += each.getFrequentRenterPoints();
        }
        return result;
    }

做完这次重构,有必要停下来思考一下。大多数重构都会减少代码总量,但这次却增加了代码总量,那是因为Java1.1需要大量语句来设置一个累加循环。哪怕只是一个简单的累加循环,每个元素只需一行代码,外围的支持代码也需要六行之多。这其实是任何程序员都熟悉的习惯写法,但代码量还是太多了。

这次重构存在另一个问题,那就是性能。原本代码只执行while循环一次,新版本要执行三次。如果while循环耗时很多,就可能大大降低程序的性能。单单为了这个原因,许多程序员不远进行这个重构动作,但是请注意我的用词。“如果”和“可能”。除非我们进行评测,否则我无法确定循环的执行时间,也无法知道这个循环是否被经常使用以至于影响到系统的整体性能。重构时你不必担心这些,优化时你才需要担心它们,但那时你已处于一个比较有利的位置,有更多选择可以完成有效优化。

现在,Customer类内的任何代码都可以调用这些查询函数了,如果系统其他部分需要这些信息,也可以轻松地将查询函数加入Customer类的接口。如果没有这些查询函数,其他函数就必须了解Rental类,并自行建立循环。在一个复杂系统中,这将使程序的编写难度和维护难度大大增加。

你可以明显看出来,htmlStatement和statement是不同的,现在我应该脱下重构的帽子,带上增加功能的帽子,我可以像下面这样编写htmlStatement:

public String htmlStatement() {
        Enumeration rentals = _rentals.elements();
        String result = "<H1>Rentals for <EM>" + getName() + "</EM></H1><P>\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            result += each.getMovie().get_title() + ": " + String.valueOf(each.getCharge()) + "<BR>\n";
        }

        result += "<P> You owe <EM>" + String.valueOf(getTotalCharge()) + "</EM><PP>\n";
        result += "On this rental you earned <EM>" + String.valueOf(getTotalFrequentRenterPoints()) + "</EM> frequent renter points<P>";
        return result;
    }

通过计算逻辑的提炼,我可以弯沉给一个htmlStatement,并复用原本statement内的所有计算。我不必剪剪贴贴,所以如果计算规则发生改变,我只需在程序中做一处修改。完成其他任何类型的详单也都很快而且很容易。这次重构并没有花很多时间,其中大半时间我用来弄清楚代码所做的事,而这是我无论如何都得做的。

你以为这就完了吗?接着往下看:

1.4 运用多态取代与价格相关的条件逻辑

这个问题的第一部分是switch语句,最好不要在另一个对象的属性基础上运用switch语句。如果不得不使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用。

class Rental...
public double getCharge() {
        double result = 0;
        switch (getMovie().get_priceCode()) {
            case Movie.REGULAR:
                result += 2;
                if (getDaysRented() > 2) {
                    result += (getDaysRented() - 2) * 1.5;
                }
                break;
            case Movie.CHILDRENS:
                result += getDaysRented() * 3;
                break;
            case Movie.NEW_RELEASE:
                result += 1.5;
                if (getDaysRented() > 3) {
                    result += (getDaysRented() - 3) * 1.5;
                }
                break;
            default:
                break;
        }
        return result;
    }

这暗示getCharge()应该移到Movie类中去:

class Movie...
public double getCharge(int dayRented) {
        double result = 0;
        switch (get_priceCode()) {
            case Movie.REGULAR:
                result += 2;
                if (dayRented > 2) {
                    result += (dayRented - 2) * 1.5;
                }
                break;
            case Movie.CHILDRENS:
                result += dayRented * 3;
                break;
            case Movie.NEW_RELEASE:
                result += 1.5;
                if (dayRented > 3) {
                    result += (dayRented - 3) * 1.5;
                }
                break;
            default:
                break;
        }
        return result;
    }

为了使它得以运作,我必须把租期长度作为参数传递进去。当然,租期长度来自Rental对象。计算费用时需要两项数据:租期长度和影片类型。为什么我选择将租期长度传给Movie对象,而不是将影片类型传给Rental对象呢?因为本系统可能发生的变化是加入新影片类型,这种变化带有不稳定倾向。如果影片类型有所变化,我希望尽量控制它造成的影响,所以选择在Movie对象内计算费用。

我把上述计算放进Movie类,然后修改Rental的getCharge,让它使用这个新函数

class Rental...
public double getCharge() {
        return _movie.getCharge(_daysRented);
    }

搬移getCharge()之后,我以相同手法处理常客积分计算,这样我就把根据影片类型而变化的所有东西,都放到了影片类型所属的类中,以下是重构后的代码:

class Rental...
    public int getFrequentRenterPoints() {
        return _movie.getFrequentRenterPoints(_daysRented);
    }
class Movie
    public int getFrequentRenterPoints(int dayRented) {
        if ((get_priceCode() == Movie.NEW_RELEASE) && dayRented > 1)
            return 2;
        else
            return 1;
    }

终于……我们来到继承

我们有数种影片类型,它们以不同的方式回答相同的问题,这听起来很像子类的工作,我们可以建立Movie的三个子类,每个都有自己的计费法

这样一来我们就可以用多态来取代switch语句了,很遗憾的是这里有个小问题,不能这么干。一部影片可以再生命周期内修改自己的分类,一个对象却不能再生命周期内修改自己所属的类。不过还是有一个解决方法:State模式。运用它之后,我们的类看起来像:

加入这一层间接性,我们就可以在Price对象内进行子类化操作,于是便可在任何必要时刻修改价格。

下面看Movie类构造方法修改前后对比:

修改前:

    public Movie(String title, int priceCode) {
        _title = title;
        _priceCode = priceCode;
    }

修改后:

    public Movie(String title, int priceCode) {
        _title = title;
        setPriceCode(priceCode);
    }

现在我们新建一个Price类,并在其中提供类型相关的行为。为了实现这一点,我在Price类内加入一个抽象函数,并在所有子类中加上对应的具体函数:

abstract class Price {
    abstract int getPriceCode();
}
class RegularPrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.REGULAR;
    }
}
class NewReleasePrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.NEW_RELEASE;
    }
}
class ChildrenPrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.CHILDRENS;
    }
}

然后就可以编译这些新建的类了。

现在,我们需要修改Movie类内的“价格代号”访问函数,让他们使用新类。下面是重构前的样子:

    private int _priceCode;
    public int get_priceCode() {
        return _priceCode;
    }

    public void set_priceCode(int _priceCode) {
        this._priceCode = _priceCode;
    }

这意味着我必须在Movie类内保存一个Price对象,而不再是保存一个_priceCode变量,此外,我还需要修改访问函数:

class Movie...
    private Price _price;
    public int getPriceCode() {
        return _price.getPriceCode();
    }

    public void setPriceCode(int arg) {
        switch (arg) {
            case REGULAR:
                _price = new RegularPrice();
                break;
            case CHILDRENS:
                _price = new ChildrenPrice();
                break;
            case NEW_RELEASE:
                _price = new NewReleasePrice();
                break;
            default:
                throw new IllegalArgumentException("Incorrect Prie Code");
        }
    }

现在我要对getCharge()重构,下面是重构前的代码:

    public double getCharge(int dayRented) {
        double result = 0;
        switch (get_priceCode()) {
            case Movie.REGULAR:
                result += 2;
                if (dayRented > 2) {
                    result += (dayRented - 2) * 1.5;
                }
                break;
            case Movie.CHILDRENS:
                result += dayRented * 3;
                break;
            case Movie.NEW_RELEASE:
                result += 1.5;
                if (dayRented > 3) {
                    result += (dayRented - 3) * 1.5;
                }
                break;
            default:
                break;
        }
        return result;
    }

搬移动作很简单,下面是重构后的代码:

class Movie...
    public double getCharge(int dayRented) {
        return _price.getCharge(dayRented);
    }

class Price...
    public double getCharge(int dayRented) {
        double result = 0;
        switch (getPriceCode()) {
            case Movie.REGULAR:
                result += 2;
                if (dayRented > 2) {
                    result += (dayRented - 2) * 1.5;
                }
                break;
            case Movie.CHILDRENS:
                result += dayRented * 3;
                break;
            case Movie.NEW_RELEASE:
                result += 1.5;
                if (dayRented > 3) {
                    result += (dayRented - 3) * 1.5;
                }
                break;

        }
        return result;
    }

我下面重构getCharge这块代码,我的做法是一次取出一个case分支,在相应的类中简历一个覆盖函数。先从RegularPrice开始:

    @Override
    public double getCharge(int dayRented) {
        double result = 2;
        if (dayRented > 2) {
            result += (dayRented - 2) * 1.5;
        }
        return result;
    }

这个函数覆盖了父类中的case语句,而我暂时还把后者留在原处不懂,然后取出下一个case分支:

class ChildrenPrice...
    @Override
    public double getCharge(int dayRented) {
        double result = 1.5;
        if (dayRented > 3) {
            result += (dayRented - 3) * 1.5;
        }
        return result;
    }

class NewReleasePrice...
    @Override
    public double getCharge(int dayRented) {
        return dayRented * 3;
    }

处理完所有case分支之后,我就报Price.getCharge()声明为abstract:

class Price...
abstract double getCharge(int dayRented);

现在我可以运用同样的手法处理getFrequentRenterPoint()。重构前的样子如下:

    public int getFrequentRenterPoints(int dayRented) {
        if ((get_priceCode() == Movie.NEW_RELEASE) && dayRented > 1)
            return 2;
        else
            return 1;
    }

首先我把这个函数移到Price类:

class Movie...
    public int getFrequentRenterPoints(int dayRented) {
        return _price.getFrequentRenterPoints(dayRented);
    }

class Price...
    public int getFrequentRenterPoints(int dayRented) {
        if ((getPriceCode() == Movie.NEW_RELEASE) && dayRented > 1)
            return 2;
        else
            return 1;
    }

但是这一次,我不把超类函数声明为abstract。我只是为新片类型增加一个覆写函数,并在超类内留下一个已定义的函数,使它成为一种默认行为:

class NewReleasePrice...
    @Override
    public int getFrequentRenterPoints(int dayRented) {
        return dayRented > 1 ? 2 : 1;
    }

class Price...
    public int getFrequentRenterPoints(int dayRented) {
        return 1;
    }

引入State模式花了不少力气,值得吗?这么做的收获是:如果我要修改任何与价格有关的行为,或者添加新的定价标准,或是加入其它取决于价格的行为,程序的修改会容易很多。这个程序的其余部分并不知道我运用了State模式。对于我目前拥有的这么几个小量行为来说,任何功能或特性上的修改也许都不合算,但如果在一个更复杂的系统中,有十来个与价格相关的函数,程序的修改难易度就会有很大的区别。以上所有的修改都是小步进行,进度似乎太过缓慢,但是我一次都没有打开过调试器,所以整个过程实际上很快就过去了。

现在我们已经完成了第二个重要的重构行为。从此,修改影片分类结构,或是改变费用计算规则、改变常客积分计算规则,都容易多了。

重构完之后的交互图及类图如下:

时间: 2024-10-09 21:35:58

重构--第一个案例的相关文章

第一章 重构,第一个案例

第一章最前面的书中前面的话说得很有道理,一本授之以渔的书,开场就来历史.原理性的东西,很难勾起继续阅读的欲望,写书是这样,开会做分享亦然,所以作者精挑细选了一个代码规模不是很大并且能告诉我们很多重构的道理. 案例说明 这是一个非常简单的案例,展示了一个影片出租店用的程序,计算每一位顾客的消费金额并打印详单的模块,同时还需要计算每一位客人的积分.给出一个 UML 图,是最初版本. 抽象了三个实体:影片(片名.片类型).租赁(影片.租赁天数).顾客(姓名.租赁清单),代码详见下面的三个类以及 Mai

学习ExtjsForVs(第一个案例HelloWord)

第一个案例-Hello Word 1.本次练习以ext-4.0.7为例,首先从网上下载ext包. 2.打开包后将里面的三个文件或文件夹拷贝到项目中. resource文件夹 bootstrap.js ext-all.js 3. 在First-Hello.html中构建如下代码 1 <!DOCTYPE html> 2 <html xmlns="http://www.w3.org/1999/xhtml"> 3 <head> 4 <meta http

使用jqplot创建报表(一) 初学后写的第一个案例源码

一.初学后写的第一个案例源码 效果图: 代码如下: <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@ include file="common.jsp"%> <html> <head> <script type="text/javascript" src=&q

重构第一天:封装集合

在一些情况下,在一个类中选择不去暴露整个集合给调用者是非常有必要的.比如当我们给一个集合添加/删除item时,我们需要添加一些额外的逻辑.因为这个原因,一个非常好的办法就是让暴露出来的collecction只能被迭代而不能被修改.让我们看下面的例子. public class Order { private List<OrderLine> _orderLines; public IEnumerable<OrderLine> OrderLines { get { return _or

Akka第一个案例动手实战开发环境的搭建

学习了Akka第一个案例动手实战开发环境的搭建, 采用IDEA 搭建AKKa的环境,快捷方便,相关的mave和其他的jar包可以自动加载. 王家林亲授<DT大数据梦工厂>大数据实战视频“Scala深入浅出实战经典”视频.音频和PPT下载!第92讲:Akka第一个案例动手实战开发环境的搭建 百度云盘:http://pan.baidu.com/s/1dDub8DN腾讯微云:http://url.cn/WYPW3Z360云盘:http://yunpan.cn/cm6u6NqEeLEeh  访问密码 

使用SBT开发Akka第一个案例源码解析MapActor、ReduceActor、AggregateActor

学习了使用SBT开发Akka第一个案例源码解析MapActor.ReduceActor.AggregateActor,应用MapActor对单词计数,发信息给ReduceActor,对信息进行local级的汇总,然后交给AggregateActor. 案例如下: class MapActor(reduceActor: ActorRef) extend Actor{ val STOP_WORDS_LIST=List("a","is") deg receive: Rec

Akka第一个案例动手实战开发消息实体类

学习了Akka第一个案例动手实战开发消息实体类,应用mapdata对单词级数,应用reduce对计数进行汇总,通过akka进行相互的消息通信. 代码案例如下: package akka.dt.app.messages; import java.util.List public class MapData{ privatie List<WordCount> dataList; public List<WordCount> getDataList()  {return dataList

Akka第一个案例动手实战main方法实现中ActorSystem等代码详解

学习了Akka第一个案例动手实战main方法实现中ActorSystem等代码详解,创建ActorSystem实例,用acterOf创建MasterActor,用tell的方式给MasterActor发信息,睡眠一段时间给MasterActor发信息,处理完后关闭,资源回收. 案例如下: public static void main(String[] args) throws Exception{ ActorSystem_system =  ActorSystem.create("HelloA

第1章 重构,第一个案例(2):分解并重组statement函数

2. 重构的第一步:建立一组可靠的测试环境 3. 分解并重组statement (1)提炼switch语句到独立函数(amountFor)和注意事项. ①先找出函数内的局部变量和参数:each和thisAmount,前者在switch语句内未被修改,后者会被修改. ②任何不会被修改的变量都可以当成参数传入新的函数,如将each为作为参数传给amountFor()的形参. ③至于会被修改的变量要格外小心,如果只有一个变量会被修改(如thisAmount),则可以当作新函数的返回值. [实例分析]影