案例:计算每位顾客的消费金额并打印详细信息。顾客租赁了哪些影片,租期多长,根据租赁时间和影片类型计算出费用。影片分为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 = new Vector(); //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 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中去,并更改名称使代码更加容易理解
AmountFor
原来的statement函数改为下面的代码
Statement
在AmountFor中我们发现它只使用了Resume类,并没有使用到Movie,所以我们将AmountFor函数放在Resume类中,并将函数名改为GetCharge
GetCharge
同时添加新的函数测试代码
testGetCharge
然后在原来的程序中找到旧函数的所有引用点,然后再用新函数去代替他们
接下来类似“费用计算”我们处理“积分计算”,直接显示修改后的代码
GetFrequentRenterPoints
statement
然后接着提取totalAmount和totalFrequentRenterPoints
Customer
最后测试一下修改后的代码
现在你会发现statement函数所做的功能全部是字符串拼接,即界面显示工作,如果需要将结果显示成HTML或者是其他形式,直接添加相同功能函数即可。
第三步:使用类的特性(分装,继承,多态)和设计模式对程序继续重构
switch部分很容易发生修改,因为在修改影片费用策略时就会修改到switch部分,我们现在来重构switch部分
switch部分最好是在自己对象上使用,尽可能的避免在别人的对象上使用。所以这就提示我们需要把switch部分移到movie类中
Movie
Resume
影片类型有三种,而这三种影片的租赁价格都有其各自的计算方法,所以使用的是策略模式
下面是重构以后关于movie修改和新加的内容:
Movie
Price
NewPrice
RegularPrice
ChildPrice
其实重构就是不断的测试修改的过程。