• 重构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" +"	" + this.getName() + "
    ";
            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 += "	" + each.get_movie().get_title() + "	" + String.valueOf(thisAmount) + "
    ";
                totalAmount += thisAmount;
            }
            
            result += "Amount owed is" + "	" + String.valueOf(totalAmount) + "
    ";
            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:"+"
    ");
            String c_name = sc.nextLine();
            Customer c1 = new Customer(c_name);
            
            System.out.println("please input the movie name:"+"
    ");
            String m_name = sc.nextLine();
            System.out.println("please input the movie type:"+ "
    ");
            System.out.println("1.regular movie"+"
    "+"2.child movie"+"
    "+"3.new movie"+"
    ");
            int type = sc.nextInt();
            Movie m1 = new Movie(m_name,type);
            System.out.println("please input the time you have rent:"+"
    ");
            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"+"
    	"+
                    "123435    3.0"+"
    "+
                    "Amount owed is    3.0"+"
    "+
                    "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" +"	" + this.getName() + "
    ";
            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 += "	" + each.get_movie().get_title() + "	" + String.valueOf(thisAmount) + "
    ";
                totalAmount += thisAmount;
            }
            
            result += "Amount owed is" + "	" + String.valueOf(totalAmount) + "
    ";
            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" +"	" + this.getName() + "
    ";
            while(resumes.hasMoreElements()){
                Resume each = resumes.nextElement();
                frequentRenterPoints += each.GetFrequentRenterPoints();
                totalAmount += each.GetCharge();
                result += "	" + each.get_movie().get_title() + "	" + String.valueOf(each.GetCharge()) + "
    ";
            }
            
            result += "Amount owed is" + "	" + String.valueOf(totalAmount) + "
    ";
            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" +"	" + this.getName() + "
    ";
            while(resumes.hasMoreElements()){
                Resume each = resumes.nextElement();
                result += "	" + each.get_movie().get_title() + "	" + String.valueOf(each.GetCharge()) + "
    ";
            }
            
            result += "Amount owed is" + "	" + String.valueOf(GetTotalCharge()) + "
    ";
            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

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

  • 相关阅读:
    浏览器的缓存机制
    浏览器渲染原理及优化
    flutter 安装 & 启动 windows
    从浏览器输入 url 到页面渲染
    压力测试简单案例
    Office2021简体中文离线安装包下载地址合集,目前最全! L
    pdfplumber yongqi
    MySQL中对varchar类型排序问题 yongqi
    基础知识串讲笔记2022124 yongqi
    Faker是一个Python包,,待学习 yongqi
  • 原文地址:https://www.cnblogs.com/sker/p/6724000.html
Copyright © 2020-2023  润新知