重构Java代码的既有设计-影片出租店

案例:计算每位顾客的消费金额并打印详细信息。顾客租赁了哪些影片,租期多长,根据租赁时间和影片类型计算出费用。影片分为3类:儿童片,新片,普通片。此外需计算该顾客的积分。

Movie:

public class Movie {
    //电影类型
    public static final int CHILD = 2;
    public static final int NEW = 3;
    public static final int REGULAR = 1;
    private String _title;
    private int _priceCode;
    
    public Movie(String title,int priceCode) {
        this._title = title;
        this._priceCode = priceCode;
    }
    
    public String get_title() {
        return _title;
    }
    /**
     * 获取影片类型
     * @return
     */
    public int get_priceCode() {
        return _priceCode;
    }
}

Resume:该顾客租赁了一部影片

public class Resume {
    private Movie _movie;
    private int _daysRented;
    
    public Resume(Movie movie,int daysRented) {
        this._movie = movie;
        this._daysRented = daysRented;
    }

    public Movie get_movie() {
        return _movie;
    }

    public int get_daysRented() {
        return _daysRented;
    }

}

Customer:

租赁费用计算:

影片类型为儿童片,两天以内费用为2,超出两天的时间,每天的费用为1.5

影片类型为新片,每天的费用为3

影片类型为普通片,三天以内费用为1.5,超出三天,每天的费用为1.5

积分计算:

每次租赁影片,积分加一,如果影片为新片且租赁时间大于1天,则多加一分

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

public class Customer {
    private String _name;
    private Vector<Resume> _resume = new Vector<Resume>();        //all resume by this customer
    
    public Customer(String name){
        this._name = name;
    }
    
    /**
     * add resume info
     * @param arg
     */
    public void addRental(Resume arg){
        this._resume.addElement(arg);
    }
    
    public String getName(){
        return this._name;
    }
    
    /**
     * get all result(include time,movie type,fee of each resume and all fee)
     * @return result
     */
    public String statement(){
        double totalAmount = 0;
        int frequentRenterPoints = 0;        //the all collectPoint; 
        Enumeration<Resume> resumes = this._resume.elements();        //all record of resumes  
        String result = "Rental Record for" +"\t" + this.getName() + "\n";
        while(resumes.hasMoreElements()){
            double thisAmount = 0;        // fee of this record
            Resume each = (Resume) resumes.nextElement();
            // the movie's type
            switch(each.get_movie().get_priceCode()){
            case Movie.CHILD:
                thisAmount += 2;    //the basic fee is 2
                if(each.get_daysRented() > 2){
                    //the day is more than 2
                    thisAmount += (each.get_daysRented() - 2) * 1.5;
                }
                break;
            case Movie.NEW:
                thisAmount += each.get_daysRented() * 3;  
                break;
            case Movie.REGULAR:
                thisAmount += 1.5;    //the basic fee is 1.5
                if(each.get_daysRented() > 3){
                    //the day is more than 3
                    thisAmount += (each.get_daysRented() - 3) * 1.5;
                }
                break;
            }
            frequentRenterPoints ++;
            if((each.get_movie().get_priceCode() == Movie.NEW)&&(each.get_daysRented() > 1)){
                frequentRenterPoints ++;
            }
            result += "\t" + each.get_movie().get_title() + "\t" + String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;
        }
        
        result += "Amount owed is" + "\t" + String.valueOf(totalAmount) + "\n";
        result += "You earned "+ String.valueOf(frequentRenterPoints) + "  frequent renter points";
         return result;
    }
    
    @SuppressWarnings("resource")
    public static void main(String arg[]){
        Scanner sc = new Scanner(System.in);
        System.out.println("please input your name:"+"\n");
        String c_name = sc.nextLine();
        Customer c1 = new Customer(c_name);
        
        System.out.println("please input the movie name:"+"\n");
        String m_name = sc.nextLine();
        System.out.println("please input the movie type:"+ "\n");
        System.out.println("1.regular movie"+"\n"+"2.child movie"+"\n"+"3.new movie"+"\n");
        int type = sc.nextInt();
        Movie m1 = new Movie(m_name,type);
        System.out.println("please input the time you have rent:"+"\n");
        int day = sc.nextInt();
        Resume r1 = new Resume(m1,day);
        c1.addRental(r1);
        String ans= c1.statement();
        System.out.println(ans);
    }
}

现在的代码可以实现基本的功能,当租赁策略、积分策略发生改变时,需要仔细查找statement策略,这时很容易引入bug。那么就很有必要重构之前写的代码。

第一步:为即将修改的代码建立一个可靠的测试环境。

MovieTest

import static org.junit.Assert.*;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class MovieTest {
    Movie m0 = new Movie("fall in love",3);

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void testGet_title() {
        assertEquals("fall in love",m0.get_title());
    }

    @Test
    public void testGet_priceCode() {
        assertEquals(3,m0.get_priceCode());
    }

}

ResumeTest

import static org.junit.Assert.*;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class ResumeTest {
    Movie m2 = new Movie("three children and their mother",2);
    Resume r2 = new Resume(m2,3);

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void testGet_movie() {
        Movie m3 = new Movie("three children and their mother",2);
        assertEquals(m3.get_title(),r2.get_movie().get_title());
    }

    @Test
    public void testGet_daysRented() {
        assertEquals(r2.get_daysRented(),3);
    }

}

CustomerTest

import static org.junit.Assert.*;
import org.junit.Test;

public class CustomerTest {
    Movie m1 = new Movie("123435",1);
    Resume r1 = new Resume(m1,4);
    Customer c1 = new Customer("abby");

    

    @Test
    public void testAddRental() {
        c1.addRental(r1);
    }

    @Test
    public void testGetName() {
        String testname = "abby";
        assertEquals(testname, c1.getName());
    }

    @Test
    public void testStatement() {
        String testResult = "Rental Record for    abby"+"\n\t"+
                "123435    3.0"+"\n"+
                "Amount owed is    3.0"+"\n"+
                "You earned 1  frequent renter points";
        c1.addRental(r1);
        String realResult = c1.statement();
        assertEquals(testResult,realResult);
    }
}

 第二步:分解重组代码块

statement函数太长了,我们需要分解它,首先将switch语句包装到另外一个函数AmountFor中去,并更改名称使代码更加容易理解

/**
     * calculate amount fee for this resume
     * @param resume
     * @return
     */
    private double AmountFor(Resume resume){
        double result = 0;        // fee of this record
        switch(resume.get_movie().get_priceCode()){
        case Movie.CHILD:
            result += 2;    //the basic fee is 2
            if(resume.get_daysRented() > 2){
                //the day is more than 2
                result += (resume.get_daysRented() - 2) * 1.5;
            }
            break;
        case Movie.NEW:
            result += resume.get_daysRented() * 3;    //the basic fee is 2
            break;
        case Movie.REGULAR:
            result += 1.5;    //the basic fee is 1.5
            if(resume.get_daysRented() > 3){
                //the day is more than 3
                result += (resume.get_daysRented() - 3) * 1.5;
            }
            break;
        }
        return result;
    }
AmountFor

原来的statement函数改为下面的代码

/**
     * get all result(include time,movie type,fee of each resume and all fee)
     * @return result
     */
    public String statement(){
        double totalAmount = 0;
        int frequentRenterPoints = 0;        //the all collectPoint; 
        Enumeration<Resume> resumes = this._resume.elements();        //all record of resumes  
        String result = "Rental Record for" +"\t" + this.getName() + "\n";
        while(resumes.hasMoreElements()){
            Resume each = resumes.nextElement();
            // get amount for each resume
            double thisAmount = this.AmountFor(each);
            frequentRenterPoints ++;
            if((each.get_movie().get_priceCode() == Movie.NEW)&&(each.get_daysRented() > 1)){
                frequentRenterPoints ++;
            }
            result += "\t" + each.get_movie().get_title() + "\t" + String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;
        }
        
        result += "Amount owed is" + "\t" + String.valueOf(totalAmount) + "\n";
        result += "You earned "+ String.valueOf(frequentRenterPoints) + "  frequent renter points";
         return result;
    }
Statement

在AmountFor中我们发现它只使用了Resume类,并没有使用到Movie,所以我们将AmountFor函数放在Resume类中,并将函数名改为GetCharge

public class Resume {
    ......
    /**
     * calculate charge for this resume
     * @return
     */
    public double GetCharge(){
        double result = 0;        // fee of this record
        switch(get_movie().get_priceCode()){
        case Movie.CHILD:
            result += 2;    //the basic fee is 2
            if(get_daysRented() > 2){
                //the day is more than 2
                result += (get_daysRented() - 2) * 1.5;
            }
            break;
        case Movie.NEW:
            result += get_daysRented() * 3;    //the basic fee is 2
            break;
        case Movie.REGULAR:
            result += 1.5;    //the basic fee is 1.5
            if(get_daysRented() > 3){
                //the day is more than 3
                result += (get_daysRented() - 3) * 1.5;
            }
            break;
        }
        return result;
    }

}
GetCharge

同时添加新的函数测试代码

public class ResumeTest {
    ......
    @Test
    public void testGetCharge() {
        assertEquals(String.valueOf(r2.GetCharge()),String.valueOf(3.5));
    }
}
testGetCharge

然后在原来的程序中找到旧函数的所有引用点,然后再用新函数去代替他们

接下来类似“费用计算”我们处理“积分计算”,直接显示修改后的代码

public class Resume {
    ......
    
    /**
     * get FrequentRenterPoints for this resume
     * @return
     */
    public int GetFrequentRenterPoints(){
        int result = 0;
        result ++;
        if((get_movie().get_priceCode() == Movie.NEW)&&(get_daysRented() > 1)){
            result ++;
        }
        return result;
    }

}
GetFrequentRenterPoints
         /**
     * get all result(include time,movie type,fee of each resume and all fee)
     * @return result
     */
    public String statement(){
        double totalAmount = 0;
        int frequentRenterPoints = 0;        //the all collectPoint; 
        Enumeration<Resume> resumes = this._resume.elements();        //all record of resumes  
        String result = "Rental Record for" +"\t" + this.getName() + "\n";
        while(resumes.hasMoreElements()){
            Resume each = resumes.nextElement();
            frequentRenterPoints += each.GetFrequentRenterPoints();
            totalAmount += each.GetCharge();
            result += "\t" + each.get_movie().get_title() + "\t" + String.valueOf(each.GetCharge()) + "\n";
        }
        
        result += "Amount owed is" + "\t" + String.valueOf(totalAmount) + "\n";
        result += "You earned "+ String.valueOf(frequentRenterPoints) + "  frequent renter points";
         return result;
    }
statement

然后接着提取totalAmount和totalFrequentRenterPoints

public class Customer {
    ......
    
    /**
     * get total charge
     * @return
     */
    private double GetTotalCharge(){
        Enumeration<Resume> resumes = this._resume.elements();        //all record of resumes
        double result = 0;
        while(resumes.hasMoreElements()){
            Resume each = resumes.nextElement();
            result += each.GetCharge();
        }
        return result;
    }
    
    /**
     * get total frequentRenterPoints
     * @return
     */
    private int GetTotalFrequentRenterPoints(){
        Enumeration<Resume> resumes = this._resume.elements();        //all record of resumes
        int result = 0;
        while(resumes.hasMoreElements()){
            Resume each = resumes.nextElement();
            result += each.GetFrequentRenterPoints();
        }
        return result;
    }
    
    /**
     * get all result(include time,movie type,fee of each resume and all fee)
     * @return result
     */
    public String statement(){
        Enumeration<Resume> resumes = this._resume.elements();        //all record of resumes  
        String result = "Rental Record for" +"\t" + this.getName() + "\n";
        while(resumes.hasMoreElements()){
            Resume each = resumes.nextElement();
            result += "\t" + each.get_movie().get_title() + "\t" + String.valueOf(each.GetCharge()) + "\n";
        }
        
        result += "Amount owed is" + "\t" + String.valueOf(GetTotalCharge()) + "\n";
        result += "You earned "+ String.valueOf(GetTotalFrequentRenterPoints()) + "  frequent renter points";
         return result;
    }
    
}
Customer

最后测试一下修改后的代码

现在你会发现statement函数所做的功能全部是字符串拼接,即界面显示工作,如果需要将结果显示成HTML或者是其他形式,直接添加相同功能函数即可。

第三步:使用类的特性(分装,继承,多态)和设计模式对程序继续重构

switch部分很容易发生修改,因为在修改影片费用策略时就会修改到switch部分,我们现在来重构switch部分

switch部分最好是在自己对象上使用,尽可能的避免在别人的对象上使用。所以这就提示我们需要把switch部分移到movie类中

public class Movie {
    ......
    
    /**
     * calculate charge for resume
     * @return
     */
    public double GetCharge(int dayRent){
        double result = 0;        // fee of this record
        switch(this.get_priceCode()){
        case Movie.CHILD:
            result += 2;    //the basic fee is 2
            if(dayRent > 2){
                //the day is more than 2
                result += (dayRent - 2) * 1.5;
            }
            break;
        case Movie.NEW:
            result += dayRent * 3;    //the basic fee is 2
            break;
        case Movie.REGULAR:
            result += 1.5;    //the basic fee is 1.5
            if(dayRent > 3){
                //the day is more than 3
                result += (dayRent - 3) * 1.5;
            }
            break;
        }
        return result;
    }
    
    /**
     * get FrequentRenterPoints for resume
     * @return
     */
    public int GetFrequentRenterPoints(int dayRent){
        if((get_priceCode() == Movie.NEW)&&(dayRent > 1))
            return 2;
        else
            return 1;
    }
}
Movie
public class Resume {
    ......
    
    /**
     * calculate charge for this resume
     * @return
     */
    public double GetCharge(){
        return _movie.GetCharge(this._daysRented);
    }
    
    /**
     * get FrequentRenterPoints for this resume
     * @return
     */
    public int GetFrequentRenterPoints(){
        return _movie.GetFrequentRenterPoints(_daysRented);
    }

}
Resume

影片类型有三种,而这三种影片的租赁价格都有其各自的计算方法,所以使用的是策略模式

下面是重构以后关于movie修改和新加的内容:

public class Movie {
    //电影类型
    public static final int CHILD = 2;
    public static final int NEW = 3;
    public static final int REGULAR = 1;
    private String _title;
    private int _priceCode;        //影片类型
    private Price _price;
    
    public Movie(String title,int priceCode) {
        this._title = title;
        this._priceCode = priceCode;
        set_priceCode();
    }
    
    public String get_title() {
        return _title;
    }
    
    public int get_priceCode() {
        return _price.getPriceCode();
    }
    
    public void set_priceCode() {
        switch(_priceCode){
        case Movie.CHILD:
            _price = new ChildPrice();
            break;
        case Movie.NEW:
            _price = new NewPrice();
            break;
        case Movie.REGULAR:
            _price = new RegularPrice();
            break;
        }
    }

    /**
     * calculate charge for resume
     * @return
     */
    public double GetCharge(int dayRent){
        return _price.getCharge(dayRent);
    }
    
    /**
     * get FrequentRenterPoints for resume
     * @return
     */
    public int GetFrequentRenterPoints(int dayRent){
        if((get_priceCode() == Movie.NEW)&&(dayRent > 1))
            return 2;
        else
            return 1;
    }
}
Movie
public abstract class Price {
    abstract int getPriceCode();
    abstract double getCharge(int dayRent);
}
Price
public class NewPrice extends Price {

    @Override
    int getPriceCode() {
        // TODO Auto-generated method stub
        return Movie.NEW;
    }
    
    @Override
    double getCharge(int dayRent) {
        return dayRent * 3;
    }

}
NewPrice
public class RegularPrice extends Price {

    @Override
    int getPriceCode() {
        // TODO Auto-generated method stub
        return Movie.REGULAR;
    }
    
    @Override
    double getCharge(int dayRent) {
        double result = 1.5;    //the basic fee is 1.5
        if(dayRent > 3){        //the day is more than 3
            result += (dayRent - 3) * 1.5;
        }
        return result;
    }

}
RegularPrice
public class ChildPrice extends Price {

    @Override
    int getPriceCode() {
        // TODO Auto-generated method stub
        return Movie.CHILD;
    }
    
    @Override
    double getCharge(int dayRent) {
        double result = 2;    //the basic fee is 2
        if(dayRent > 2){    //the day is more than 2
            result += (dayRent - 2) * 1.5;
        }
        return result;
    }

}
ChildPrice

其实重构就是不断的测试修改的过程。

posted @ 2017-04-19 13:23  珍真小心  阅读(1411)  评论(0编辑  收藏  举报