案例:计算每位顾客的消费金额并打印详细信息。顾客租赁了哪些影片,租期多长,根据租赁时间和影片类型计算出费用。影片分为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; }
原来的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; }
在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; } }
同时添加新的函数测试代码
public class ResumeTest { ...... @Test public void testGetCharge() { assertEquals(String.valueOf(r2.GetCharge()),String.valueOf(3.5)); } }
然后在原来的程序中找到旧函数的所有引用点,然后再用新函数去代替他们
接下来类似“费用计算”我们处理“积分计算”,直接显示修改后的代码
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; } }
/** * 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; }
然后接着提取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; } }
最后测试一下修改后的代码
现在你会发现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; } }
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); } }
影片类型有三种,而这三种影片的租赁价格都有其各自的计算方法,所以使用的是策略模式
下面是重构以后关于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; } }
public abstract class Price { abstract int getPriceCode(); abstract double getCharge(int dayRent); }
public class NewPrice extends Price { @Override int getPriceCode() { // TODO Auto-generated method stub return Movie.NEW; } @Override double getCharge(int dayRent) { return dayRent * 3; } }
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; } }
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; } }
其实重构就是不断的测试修改的过程。