《重构-改善既有代码的设计 第1章 重构,第一个案例》学习笔记

源代码

1.1 起点

影片出租店UML类图.jpg

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 code) {
        this._title = _title;
        _priceCode = code;
    }

    public int getPriceCode() {
        return _priceCode;
    }

    public void setPriceCode(int code) {
        _priceCode = code;
    }

    public String get_title() {
        return _title;
    }
    
}

Rental(租赁)

class Rental {
   private Movie _movie;
   private int _daysRented;
   public Rental(Movie movie, int daysRented) {
       _movie = movie;
       _daysRented = daysRented;
   }
   public Movie getMovie() {
       return _movie;
   }
   public int getDaysRented() {
       return _daysRented;
   }
}

Customer(顾客)

import java.util.Enumeration;
import java.util.Vector;

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

            /**
             * 租碟的价格是根据碟的类型进行计算的,
             * 一般的碟:
             * 2块钱起租赁,超过两天每天1块五
             * 新碟:
             * 每天三块
             * 儿童碟:
             * 不足四天都是1.5,超过四天(包括)每天1.5
             */
            //determine amounts for each line
            switch (each.getMovie().getPriceCode()) { 
                case Movie.REGULAR:
                    thisAmount += 2;
                    if (each.getDaysRented() > 2) {
                        thisAmount += (each.getDaysRented() - 2) * 1.5;
                    }
                    break;
                case Movie.NEW_RELEASE:
                    thisAmount += each.getDaysRented() * 3;
                    break;
                case Movie.CHILDRENS:
                    thisAmount += 1.5;
                    if (each.getDaysRented() > 3){
                        thisAmount += (each.getDaysRented() - 3) * 1.5;
                        break;
                    }
            }
                    
            // add frequent renter points
            frequentRenterPoints ++;
            
            // add bonus for a two day new release rental
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) {
                frequentRenterPoints ++;
            }
            
            // show figures fo this rental
            result += "\t" + each.getMovie().getTitle() + "\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;
    }
}

statement()是生成详单的函数,交互图如下:

图1-2 statement()的交互过程.jpg

测试代码

public class CustomerTest {

    @org.junit.Test
    public void statement() {
        //电影
        Movie m1 = new Movie("电影1", Movie.CHILDRENS);
        Movie m2 = new Movie("电影2", Movie.CHILDRENS);
        Movie m3 = new Movie("电影3", Movie.CHILDRENS);
        Movie m4 = new Movie("电影4", Movie.CHILDRENS);
        Movie m5 = new Movie("电影5", Movie.CHILDRENS);
        Movie m6 = new Movie("电影6", Movie.NEW_RELEASE);
        Movie m7 = new Movie("电影7", Movie.NEW_RELEASE);
        Movie m8 = new Movie("电影8", Movie.REGULAR);
        Movie m9 = new Movie("电影9", Movie.REGULAR);
        Movie m10 = new Movie("电影10", Movie.REGULAR);
        Movie m11 = new Movie("电影11", Movie.REGULAR);
        Customer customer = new Customer("Mike");
        customer.addRental(new Rental(m1,1));
        customer.addRental(new Rental(m2,2));
        customer.addRental(new Rental(m3,3));
        customer.addRental(new Rental(m4,4));
        customer.addRental(new Rental(m5,5));
        customer.addRental(new Rental(m6,1));
        customer.addRental(new Rental(m7,2));
        customer.addRental(new Rental(m8,1));
        customer.addRental(new Rental(m9,2));
        customer.addRental(new Rental(m10,3));
        customer.addRental(new Rental(m11,4));
        String exepectedResult = 
            "Rental Record for Mike\n" +
            "\t电影1\t1.5\n" +
            "\t电影2\t1.5\n" +
            "\t电影3\t1.5\n" +
            "\t电影4\t3.0\n" +
            "\t电影5\t4.5\n" +
            "\t电影6\t3.0\n" +
            "\t电影7\t6.0\n" +
            "\t电影8\t2.0\n" +
            "\t电影9\t2.0\n" +
            "\t电影10\t3.5\n" +
            "\t电影11\t5.0\n" +
        "Amount owed is 33.5\n" +
        "You earned 12 frequent renter points";
            
        org.junit.Assert.assertEquals(exepectedResult, customer.statement());
    }

}

对此起始程序的评价

不符合面向对象精神。
Customer#statement做的事情实在太多了,它做了很多原本应该由其他类完成的事情。

新需求: 以HTML格式输出详单;计费标准;改变影片分类规则;
无法适应新需求,一旦有新的需求,代码改动太大或所做的改动容易引入bug。

一个方法不能过长,不能超过一屏;
代码不能出现两次;

需要为程序添加新特性,代码结构使你很难达成目的,先重构――使新特性的添加比较容易,任何再添加新特性。

1.2 重构的第一步

第一个步骤:为即将修改的代码建立一组可靠的测试环境。
让测试有能力自我检测。
好的测试是重构的根本。
花时间建立一个优良的测试机制是完全值得的。

1.3 分解并重组statement()

代码块愈小,代码的功能就愈容易管理,代码的处理和移动也就愈轻松。

步骤1:找出代码的逻辑泥团并运用Extract Method。
首先,找出函数内的局部变量和参数;
each:未被修改
thisAmount:被修改
任何不会被修改的变量可以被当成参数传入新的函数。
如果只有一个变量会被修改,可以把它当做返回值。

以下代码:statement()提炼出amountFor()方法。

import java.util.Enumeration;
import java.util.Vector;

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;
    }
    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 = amountFor(each);
                    
            // add frequent renter points
            frequentRenterPoints ++;
            
            // add bonus for a two day new release rental
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) {
                frequentRenterPoints ++;
            }
            
            // show figures fo this rental
            result += "\t" + each.getMovie().getTitle() + "\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 amountFor(Rental each) {
        double thisAmount = 0;
        switch (each.getMovie().getPriceCode()) { 
            case Movie.REGULAR:
                thisAmount += 2;
                if (each.getDaysRented() > 2) {
                    thisAmount += (each.getDaysRented() - 2) * 1.5;
                }
                break;
            case Movie.NEW_RELEASE:
                thisAmount += each.getDaysRented() * 3;
                break;
            case Movie.CHILDRENS:
                thisAmount += 1.5;
                if (each.getDaysRented() > 3){
                    thisAmount += (each.getDaysRented() - 3) * 1.5;
                    break;
                }
        }
        return thisAmount;
    }
}

使用IDE提供的提取函数功能就可以做到。

重构步骤的本质:由于每次修改的幅度都很小,所以任何错误都很容易发现。
重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。

修改amountFor()局部变量名称

private double amountFor(Rental aRental) {
    double result = 0;
    switch (aRental.getMovie().getPriceCode()) { 
        case Movie.REGULAR:
            result += 2;
            if (aRental.getDaysRented() > 2) {
                result += (aRental.getDaysRented() - 2) * 1.5;
            }
            break;
        case Movie.NEW_RELEASE:
            result += aRental.getDaysRented() * 3;
            break;
        case Movie.CHILDRENS:
            result += 1.5;
            if (aRental.getDaysRented() > 3){
                result += (aRental.getDaysRented() - 3) * 1.5;
                break;
            }
    }
    return result;
}

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

搬移“金额计算”代码

amountFor()使用了Rental类的信息,却没有使用Customer类的。
函数应该放在它所使用的数据的所属对象内。

amountFor()移到Rental类:Move Method。

class Rental {
    private Movie _movie;
    private int _daysRented;
    public Rental(Movie movie, int daysRented) {
        _movie = movie;
        _daysRented = daysRented;
    }
    public Movie getMovie() {
        return _movie;
    }
    public int getDaysRented() {
        return _daysRented;
    }
    double getCharge() {
        double result = 0;
        switch (getMovie().getPriceCode()) { 
            case Movie.REGULAR:
                result += 2;
                if (getDaysRented() > 2) {
                    result += (getDaysRented() - 2) * 1.5;
                }
                break;
            case Movie.NEW_RELEASE:
                result += getDaysRented() * 3;
                break;
            case Movie.CHILDRENS:
                result += 1.5;
                if (getDaysRented() > 3){
                    result += (getDaysRented() - 3) * 1.5;
                    break;
                }
        }
        return result;
    }
}

去掉参数,变更函数名称。

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

下一个步骤:找出程序中对于旧函数的所有引用点,并修改它们,让它们改用新函数。

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()) {
            double thisAmount = 0;
            Rental each = (Rental) rentals.nextElement();

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

移动金额计算函数后,所有类的状态.jpg

下一个步骤:去掉旧函数;
如果旧函数是一个public函数,而你又不想修改其他类的接口,可以保留旧函数,让它调用新函数。

下一个步骤:thisAmount变得多余了。它接受each.getCharge()的执行结果,然后就不再有任何变化。
运用Replace Temp with Query把thisAmount除去:

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();
            // add frequent renter points
            frequentRenterPoints ++;
            
            // add bonus for a two day new release rental
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) {
                frequentRenterPoints ++;
            }
            
            // show figures fo this rental
            result += "\t" + each.getMovie().getTitle() + "\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类中被优化。而且如果代码有合理的组织和管理,优化就会有很好的效果。

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

首先:运用Extract Method:
局部变量: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.getFrequenterPoints();
            
            // show figures fo this rental
            result += "\t" + each.getMovie().getTitle() + "\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 {
    ...
    int getFrequenterPoints() {
        if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1) {
            return 2;
        } else 
            return 1;
    }
}
常客积分计算函数被提炼及搬移之前的类图和序列图.jpg
常客积分计算函数被提炼及搬移之后的类图和序列图.jpg

去除临时变量

临时变量只在自己所属的函数中有效,所以它们会助长冗长而复杂的函数。
运用Replace Temp with Query,并利用查询函数(query method)来取代totalAmountfrequentRentalPoints这两个临时变量。
由于类中的任何函数都可以调用上述查询函数,所以它能够促成较干净的设计,而减少冗长复杂的函数。
以下代码是去除totalAmountfrequentRentalPoints变量之后的:

class Customer {
...
    public String statement(){
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            // show figures fo this rental
            result += "\t" + each.getMovie().getTitle() + "\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 double getTotalCharge() {
        double result = 0;
        Enumeration rentals = _rentals.elements();
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            result += each.getCharge();
        }
        return result;
    }
    private int getTotalFrequentRenterPoints() {
        int result = 0;
        Enumeration rentals = _rentals.elements();
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            result += each.getFrequenterPoints();
        }
        return result;
    }

}

总量计算函数被提炼前的类图和序列图.jpg

总量计算函数被提炼后的类图和序列图.jpg

到目前为止的代码:

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 code) {
        this._title = _title;
        _priceCode = code;
    }

    public int getPriceCode() {
        return _priceCode;
    }

    public void setPriceCode(int code) {
        _priceCode = code;
    }

    public String getTitle() {
        return _title;
    }
    
}

class Rental {
    private Movie _movie;
    private int _daysRented;
    public Rental(Movie movie, int daysRented) {
        _movie = movie;
        _daysRented = daysRented;
    }
    public Movie getMovie() {
        return _movie;
    }
    public int getDaysRented() {
        return _daysRented;
    }
    double getCharge() {
        double result = 0;
        switch (getMovie().getPriceCode()) { 
            case Movie.REGULAR:
                result += 2;
                if (getDaysRented() > 2) {
                    result += (getDaysRented() - 2) * 1.5;
                }
                break;
            case Movie.NEW_RELEASE:
                result += getDaysRented() * 3;
                break;
            case Movie.CHILDRENS:
                result += 1.5;
                if (getDaysRented() > 3){
                    result += (getDaysRented() - 3) * 1.5;
                    break;
                }
        }
        return result;
    }
    int getFrequenterPoints() {
        if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1) {
            return 2;
        } else 
            return 1;
    }
}

import java.util.Enumeration;
import java.util.Vector;

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;
    }
    public String statement(){
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            // show figures fo this rental
            result += "\t" + each.getMovie().getTitle() + "\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 double getTotalCharge() {
        double result = 0;
        Enumeration rentals = _rentals.elements();
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            result += each.getCharge();
        }
        return result;
    }
    private int getTotalFrequentRenterPoints() {
        int result = 0;
        Enumeration rentals = _rentals.elements();
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            result += each.getFrequenterPoints();
        }
        return result;
    }

}
public class CustomerTest {

    @org.junit.Test
    public void statement() {
        //电影
        Movie m1 = new Movie("电影1", Movie.CHILDRENS);
        Movie m2 = new Movie("电影2", Movie.CHILDRENS);
        Movie m3 = new Movie("电影3", Movie.CHILDRENS);
        Movie m4 = new Movie("电影4", Movie.CHILDRENS);
        Movie m5 = new Movie("电影5", Movie.CHILDRENS);
        Movie m6 = new Movie("电影6", Movie.NEW_RELEASE);
        Movie m7 = new Movie("电影7", Movie.NEW_RELEASE);
        Movie m8 = new Movie("电影8", Movie.REGULAR);
        Movie m9 = new Movie("电影9", Movie.REGULAR);
        Movie m10 = new Movie("电影10", Movie.REGULAR);
        Movie m11 = new Movie("电影11", Movie.REGULAR);
        Customer customer = new Customer("Mike");
        customer.addRental(new Rental(m1,1));
        customer.addRental(new Rental(m2,2));
        customer.addRental(new Rental(m3,3));
        customer.addRental(new Rental(m4,4));
        customer.addRental(new Rental(m5,5));
        customer.addRental(new Rental(m6,1));
        customer.addRental(new Rental(m7,2));
        customer.addRental(new Rental(m8,1));
        customer.addRental(new Rental(m9,2));
        customer.addRental(new Rental(m10,3));
        customer.addRental(new Rental(m11,4));
        String exepectedResult = 
            "Rental Record for Mike\n" +
            "\t电影1\t1.5\n" +
            "\t电影2\t1.5\n" +
            "\t电影3\t1.5\n" +
            "\t电影4\t3.0\n" +
            "\t电影5\t4.5\n" +
            "\t电影6\t3.0\n" +
            "\t电影7\t6.0\n" +
            "\t电影8\t2.0\n" +
            "\t电影9\t2.0\n" +
            "\t电影10\t3.5\n" +
            "\t电影11\t5.0\n" +
        "Amount owed is 33.5\n" +
        "You earned 12 frequent renter points";
            
        org.junit.Assert.assertEquals(exepectedResult, customer.statement());
    }

}

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

用户准备修改影片分类规则,为了让费用计算和常客积分计算逻辑更加修改,特意进行重构。

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

getCharge()移到Movie类里。

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

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

把计费方法放进Movie类,然后修改RentalgetCharge(),让它使用这个新函数:

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

处理常客积分计算:

class Rental {
...
    int getFrequenterPoints() {
        return _movie.getFrequenterPoints(_daysRented);
    }
}
public class Movie {
...  
  int getFrequenterPoints(int daysRented) {
        if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1) {
            return 2;
        } else 
            return 1;
    }
}
图1-12 本节所讨论的两个函数被移到Movie类内之前系统的类图
图1-12 本节所讨论的两个函数被移到Movie类内之后系统的类图

这样就把根据影片类型而变化的所有东西,都放到了影片类型所属的类中。

终于...我们来到继承

有数种影片类型,它们以不同的方式回答相同的问题。

图1-14 以继承机制表现不同的影片类型

这样一来,可以用多态来替换switch语句了。但是不能这么干,因为一部影片可以在生命周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。 可以考虑State模式。

图1-15 运用State模式表现不同的影片

这是一个State模式还是一个Strategy模式?答案取决于Price类究竟代表计费方式,还是代表影片的某个状态。
引入State模式的重构手法:
首先运用Replace Type Code with State/Strategy,将与类型相关的行为搬移至State模式内。然后运行Move Method将switch语句移到Price类。最后运用Replace Conditional with Polymorphism去掉switch语句。

首先运用Replace Type Code with State/Strategy。第一步骤是针对类型代码使用Self Encapsulate Field,确保任何时候都通过取值函数和设置函数来访问类型代码。

public class Movie {
....
    public Movie(String _title, int code) {
        this._title = _title;
        setPriceCode(code);
    }
....
}

加入Price类,并在其中提供类型相关的行为。

public abstract class Price {
    abstract int getPriceCode();
}
public class ChildrensPrice extends Price {

    @Override
    int getPriceCode() {
        return Movie.CHILDRENS;
    }

}
public class NewReleasePrice extends Price {

    @Override
    int getPriceCode() {
        return Movie.NEW_RELEASE;
    }

}

public class RegularPrice extends Price {

    @Override
    int getPriceCode() {
        return Movie.REGULAR;
    }

}

现在,修改Movie类内的“价格代号”访问函数,让它们使用新类。
Movie类中使用Price对象。

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 Price _price;

    public Movie(String _title, int code) {
        this._title = _title;
        setPriceCode(code);
    }

    public int getPriceCode() {
        return _price.getPriceCode();
    }

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

    public String getTitle() {
        return _title;
    }

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

    int getFrequenterPoints(int daysRented) {
        if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1) {
            return 2;
        } else
            return 1;
    }
}


Movie#getCharge()实施Move Method。

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

运用Replace Conditional with Polymorphism。

public abstract class Price {
    abstract int getPriceCode();
    abstract double getCharge(int daysRented);
}

public class RegularPrice extends Price {

    @Override
    int getPriceCode() {
        return Movie.REGULAR;
    }

    double getCharge(int daysRented) {
        double result = 2;
        if (daysRented > 2) {
            result += (daysRented - 2) * 1.5;
        }
        return result;
    }
}


public class NewReleasePrice extends Price {

    @Override
    int getPriceCode() {
        return Movie.NEW_RELEASE;
    }
    
    double getCharge(int daysRented) {
            return daysRented * 3;
    }
}

public class ChildrensPrice extends Price {

    @Override
    int getPriceCode() {
        return Movie.CHILDRENS;
    }
    double getCharge(int daysRented) {
        double result = 1.5;
        if (daysRented > 3) {
            result += (daysRented - 3) * 1.5;
        }
        return result;
    }
}

运用相同手法处理Movie#getFrequenterPoints()
把这个函数移到Price类。

public class Movie {
...
    int getFrequenterPoints(int daysRented) {
        return _price.getFrequenterPoints(daysRented);
    }
}

public abstract class Price {
    abstract int getPriceCode();
    abstract double getCharge(int daysRented);
    int getFrequenterPoints(int daysRented) {
        if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1) {
            return 2;
        } else
            return 1;
    }
}

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


public class NewReleasePrice extends Price {

    @Override
    int getPriceCode() {
        return Movie.NEW_RELEASE;
    }
    
    double getCharge(int daysRented) {
            return daysRented * 3;
    }

    @Override
    int getFrequenterPoints(int daysRented) {
        return (daysRented > 1)? 2 : 1;
    }
    
}
public abstract class Price {
    abstract int getPriceCode();
    abstract double getCharge(int daysRented);
    int getFrequenterPoints(int daysRented) {
        return 1;
    }
}

引入State模式的好处:如果要修改任何与价格有关的行为,或是添加新的定价标准,或是加入其它取决于价格的行为,程序的修改会容易很多。

类图1-16 加入State模式后的交互图
类图1-16 加入State模式后的类图

1.5 结语

重构使责任的分配更合理,代码的维护更轻松。
重构的节奏:测试、小修改、测试、小修改、测试、小修改...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容