本系列来自《java 重构改善既有代码的设计》一书
实例一:影片租赁出租店的程序设计。
计算每一位顾客的消费金额并打印报表(statement),操作者告诉程序:租客租了哪些影片、租期多长、程序便根据租赁时间和影片类型算出费用。
影片分三类:普通片、儿童片和新片。除了计算费用,我们还需要为常客计算点数:点数会随着(租片种类是否为新片)有所不同。
初始:
Movie实体类:
package com.xuzengqiang.ssb.movie;
/**
* 影片实体
* @author xuzengqiang
* @since 2014-12-16 14:28:49
*/
public class Movie {
public static final int CHILDRENS = 0;
public static final int REGULAR = 1;
public static final int NEW_MOVIE = 2;
private String title;
private int priceCode; //代号
public Movie(String title, int priceCode) {
this.title = title;
this.priceCode = priceCode;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int priceCode) {
this.priceCode = priceCode;
}
}
Rental租赁实体类:
package com.xuzengqiang.ssb.movie;
/**
* 租赁实体
* @author xuzengqiang
* @since 2014-12-16 14:39:38
*/
public class Rental {
private Movie movie; // 影片
private int daysRented; // 租期
public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}
public Movie getMovie() {
return movie;
}
public void setMovie(Movie movie) {
this.movie = movie;
}
public int getDaysRented() {
return daysRented;
}
public void setDaysRented(int daysRented) {
this.daysRented = daysRented;
}
}
Customer客户:
package com.xuzengqiang.ssb.movie;
import java.util.Enumeration;
import java.util.Vector;
/**
* 租客
* @author xuzengqiang
* @since 2014-12-16 14:41:22
*/
public class Customer {
/**
* 描述:姓名
*/
private String name;
/**
* 描述:租赁记录
*/
private Vector<Rental> rentalVector = new Vector<Rental>();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental rental) {
rentalVector.add(rental);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 描述:打印报表
* @return
*/
@SuppressWarnings("unused")
public String statement() {
// 总消费金额
double totalAmount = 0;
// 常客积分点
int renterPointer = 0;
Enumeration<Rental> rental = rentalVector.elements();
StringBuffer result = new StringBuffer();
while (rental.hasMoreElements()) {
double amount = 0;
Rental temp = rental.nextElement();
switch (temp.getMovie().getPriceCode()) {
// 如果是儿童片:前3天1.5,超过3天每天1.5
case Movie.CHILDRENS:
amount += 1.5;
//
if (temp.getDaysRented() > 3) {
amount += (temp.getDaysRented() - 3) * 1.5;
}
break;
// 如果是普通片:不超过2天2块钱,超过2天每天1.5
case Movie.REGULAR:
amount += 2;
if (temp.getDaysRented() > 2) {
amount += (temp.getDaysRented() - 2) * 1.5;
}
break;
// 如果是新片:每天3块S
case Movie.NEW_MOVIE:
amount += temp.getDaysRented() * 3;
break;
}
renterPointer++;
// 如果是新片,且租赁时间超过1天积分+1
if (temp.getMovie().getPriceCode() == Movie.NEW_MOVIE && temp.getDaysRented() > 1) {
renterPointer++;
}
// result拼接....
totalAmount += amount;
}
// result拼接...
return result.toString();
}
}
为什么要重构上面的代码:
可能这段代码真的能实现现在的业务逻辑和功能,但是Customer中的statement()方法做了太多的事情,一个类中的某个方法应该只负责一件事情。
再者如果以后的需求有变化,如需要以HTML的格式打印报表,从而可以在网页上显示,非常符合潮流。但是通过代码你会发现,目前的statement()根本无法实现打印HTML报表,这时我们唯一可以做的就是写一个全新的htmlStatement()方法,大量重复的使用statement()中的内容。这时候我们也许又会想就也就是简单的拷贝就行了。
如果这是收费的标准变了,这个时候我们又得同时修改statement()和htmlStatement()两个方法,并保证两处修改的一致性。当后续还需要修改,此时带来的问题就会越来越多,随着功能越来越复杂,规则越来越多,修改点也会越来越难找,不犯错的机会也越来越小,这时我们就需要对代码进行重构。
重构步骤:
1、需要一个可靠的测试,保证重构后的代码是正确的。
2、分解statement():
代码区块越小,代码的功能就越容易管理,后期维护也会更加方便。
首先抽离switch语句,考虑到temp并为被修改,amount会被修改,这时候可以将不会被修改的变量作为参数传入新的函数,至于会被修改的变量,如果只有一个变量被修改,我们可以考虑将它做为一个返回值。
修改如下:
package com.xuzengqiang.ssb.movie;
import java.util.Enumeration;
import java.util.Vector;
/**
* 租客
* @author xuzengqiang
* @since 2014-12-16 14:41:22
*/
public class Customer {
/**
* 描述:姓名
*/
private String name;
/**
* 描述:租赁记录
*/
private Vector<Rental> rentalVector = new Vector<Rental>();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental rental) {
rentalVector.add(rental);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 描述:打印报表
* @return
*/
@SuppressWarnings("unused")
public String statement() {
// 总消费金额
double totalAmount = 0;
// 常客积分点
int renterPointer = 0;
Enumeration<Rental> rental = rentalVector.elements();
StringBuffer result = new StringBuffer();
while (rental.hasMoreElements()) {
Rental temp = rental.nextElement();
double amount = getAmount(temp);
renterPointer++;
// 如果是新片,且租赁时间超过1天积分+1
if (temp.getMovie().getPriceCode() == Movie.NEW_MOVIE && temp.getDaysRented() > 1) {
renterPointer++;
}
// result拼接....
totalAmount += amount;
}
// result拼接...
return result.toString();
}
/**
* 描述:根据租赁实体获取单笔消费金额
* @param rental:租赁对象
* @return
*/
public double getAmount(Rental temp) {
double amount = 0;
switch (temp.getMovie().getPriceCode()) {
// 如果是儿童片:前3天1.5,超过3天每天1.5
case Movie.CHILDRENS:
amount += 1.5;
//
if (temp.getDaysRented() > 3) {
amount += (temp.getDaysRented() - 3) * 1.5;
}
break;
// 如果是普通片:不超过2天2块钱,超过2天每天1.5
case Movie.REGULAR:
amount += 2;
if (temp.getDaysRented() > 2) {
amount += (temp.getDaysRented() - 2) * 1.5;
}
break;
// 如果是新片:每天3块S
case Movie.NEW_MOVIE:
amount += temp.getDaysRented() * 3;
break;
}
return amount;
}
}
重构的时候以微小的步伐修改程序,这样即使犯下错误,也可以很容易的发现。
对于上面的代码,如果我不喜欢getAmount()中的某些变量名,如参数名temp.获取对于自己而言很好理解,可是可能你这段代码出现错误,而由别个帮你修改的时候可能不能一眼就看出你这个temp是什么意思,所以更改变量名是非常有必要的,因为他可以清楚的表达出自己的功能。
于是做出如下调整:
/**
* 描述:根据租赁实体获取单笔消费金额
* @param rental:租赁对象
* @return
*/
public double getAmount(Rental rental) {
double result = 0;
switch (rental.getMovie().getPriceCode()) {
// 如果是儿童片:前3天1.5,超过3天每天1.5
case Movie.CHILDRENS:
result += 1.5;
//
if (rental.getDaysRented() > 3) {
result += (rental.getDaysRented() - 3) * 1.5;
}
break;
// 如果是普通片:不超过2天2块钱,超过2天每天1.5
case Movie.REGULAR:
result += 2;
if (rental.getDaysRented() > 2) {
result += (rental.getDaysRented() - 2) * 1.5;
}
break;
// 如果是新片:每天3块S
case Movie.NEW_MOVIE:
result += rental.getDaysRented() * 3;
break;
}
return result;
}
记住一句话:
任何一个傻瓜都能写出计算机能够理解的代码,惟有写出人类容易理解的代码,才是优秀的程序员。
3、搬移金额计算代码:我们发现getAmount()这个代码片使用了Rental的信息,但是却没有使用Customer中的任何信息,所以我们需要怀疑是不是放错位置了。
调整如下:
Rental:
package com.xuzengqiang.ssb.movie;
/**
* 租赁实体
* @author xuzengqiang
* @since 2014-12-16 14:39:38
*/
public class Rental {
private Movie movie; // 影片
private int daysRented; // 租期
public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}
public Movie getMovie() {
return movie;
}
public void setMovie(Movie movie) {
this.movie = movie;
}
public int getDaysRented() {
return daysRented;
}
public void setDaysRented(int daysRented) {
this.daysRented = daysRented;
}
/**
* 描述:根据租赁实体获取单笔消费金额
* @return
*/
public double getCharge() {
double result = 0;
switch (getMovie().getPriceCode()) {
// 如果是儿童片:前3天1.5,超过3天每天1.5
case Movie.CHILDRENS:
result += 1.5;
//
if (getDaysRented() > 3) {
result += (getDaysRented() - 3) * 1.5;
}
break;
// 如果是普通片:不超过2天2块钱,超过2天每天1.5
case Movie.REGULAR:
result += 2;
if (getDaysRented() > 2) {
result += (getDaysRented() - 2) * 1.5;
}
break;
// 如果是新片:每天3块S
case Movie.NEW_MOVIE:
result += getDaysRented() * 3;
break;
}
return result;
}
}
而在Customer中就可以使用getCharge获取金额了
public double getAmount(Rental rental) {
return rental.getCharge();
}
这个时候getAmount实际上就是调用rental.getCharge()方法,所以可以略去该方法,在statement()中直接写:
Rental temp = rental.nextElement();
double amount = temp.getCharge();
这时我们会发现amount这个变量就会变的有点多余了,所以可以删去,这个时候的statement就会变成:
/**
* 描述:打印报表
* @return
*/
@SuppressWarnings("unused")
public String statement() {
// 总消费金额
double totalAmount = 0;
// 常客积分点
int renterPointer = 0;
Enumeration<Rental> rental = rentalVector.elements();
StringBuffer result = new StringBuffer();
while (rental.hasMoreElements()) {
Rental temp = rental.nextElement();
renterPointer++;
// 如果是新片,且租赁时间超过1天积分+1
if (temp.getMovie().getPriceCode() == Movie.NEW_MOVIE && temp.getDaysRented() > 1) {
renterPointer++;
}
// result拼接....
totalAmount += temp.getCharge();
}
// result拼接...
return result.toString();
}
尽量去除一些没有用的变量,减少不必要的麻烦。
4、提炼常客积分代码:
renterPointer++;
// 如果是新片,且租赁时间超过1天积分+1
if (temp.getMovie().getPriceCode() == Movie.NEW_MOVIE && temp.getDaysRented() > 1) {
renterPointer++;
}
这里再一次搜索局部变量temp,可以用做参数传入到新函数中,这里由于renterPointer已经有了初始值,所以没有必要进行进行传参,只需要对它执行加法即可。
通过对获取金额的提取经验,我们将获取常客积分代码抽取出来放到Rental类中:
/**
* 描述:获取常客积分
*/
public int getRenterPointer() {
if (getMovie().getPriceCode() == Movie.NEW_MOVIE && getDaysRented() > 1) {
return 2;
}
return 1;
}
此时的statement为:
public String statement() {
// 总消费金额
double totalAmount = 0;
// 常客积分点
int renterPointer = 0;
Enumeration<Rental> rental = rentalVector.elements();
StringBuffer result = new StringBuffer();
while (rental.hasMoreElements()) {
Rental temp = rental.nextElement();
renterPointer += temp.getRenterPointer();
// result拼接....
totalAmount += temp.getCharge();
}
// result拼接...
return result.toString();
}
5、去除临时变量,提取totalAmount和renterPointer
/**
* 描述:获取总金额
*/
public double getTotalCharge() {
double result = 0;
Enumeration<Rental> rental = rentalVector.elements();
while (rental.hasMoreElements()) {
Rental temp = rental.nextElement();
result += temp.getCharge();
}
return result;
}
/**
* 描述:获取常客积分
*/
public double getTotalRenterPointer() {
double result = 0;
Enumeration<Rental> rental = rentalVector.elements();
while (rental.hasMoreElements()) {
Rental temp = rental.nextElement();
result += temp.getRenterPointer();
}
return result;
}
这个时候的statement()为:
/**
* 描述:打印报表
* @return
*/
public String statement() {
StringBuffer result = new StringBuffer();
result.append("姓名:" + getName() + ",总金额:" + getTotalCharge() + ",积分为:" + getTotalRenterPointer());
return result.toString();
}
此时我们需要考虑的事情,大多数重构都会减少代码量,但是这次却增加了代码总量,而且相对于前面的代码而言,多计算了一次循环,性能更低。如果数据量非常大,性能的损耗是非常明显的。但是我们不能因为这个原因而不进行或不愿进行这个重构动作,因为写出这两个方法必须考虑到系统其它处可能需要调用这个method,所以单独提取出来是非常有必要的,如果不提供这些query method,那么其它函数就必须了解Rental这个类,并自行建立循环,在一个复杂的系统中,会使得编写难度加大。而且这个不是重构的时候需要担心的问题,这是优化的时候需要考虑的问题。
至此,我们不需要大量的复制粘贴就能够构建出一个htmlStatement()方法,如果计算规则发生改变,我们也只需要对程序的一处做修改等.....
6、运用多态取代与价格相关的条件逻辑
首先考虑Rental上的getCharge()方法,第一考虑switch语句:
switch (getMovie().getPriceCode()) {
在另一个对象的属性基础上运用switch语句,并不很好,如果不得不使用,也应该在对象自己的基础上去使用,而不是在别人的数据上去使用。
所以这个getCharge()方法得移动到Movie上去,当然为了程序运行正常,我们得将daysRented传入进去,于是变成了这样:
Movie
/**
* 根据影片类型的不同获取费用
* @param daysRented:租赁时间
* @return
*/
public double getCharge(int daysRented) {
double result = 0;
switch (getPriceCode()) {
// 如果是儿童片:前3天1.5,超过3天每天1.5
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3) {
result += (daysRented - 3) * 1.5;
}
break;
// 如果是普通片:不超过2天2块钱,超过2天每天1.5
case Movie.REGULAR:
result += 2;
if (daysRented > 2) {
result += (daysRented - 2) * 1.5;
}
break;
// 如果是新片:每天3块S
case Movie.NEW_MOVIE:
result += daysRented * 3;
break;
}
return result;
}
而Rental中的getCharge():
/**
* 描述:根据租赁实体获取单笔消费金额
* @return
*/
public double getCharge() {
return movie.getCharge(getDaysRented());
}
同样以相同的手法处理常客积分:
Movie
/**
* 描述:根据影片的类型以及租期获取常客积分
* @param daysRented
* @return
*/
public int getRenterPointer(int daysRented) {
if (getPriceCode() == Movie.NEW_MOVIE && daysRented > 1) {
return 2;
}
return 1;
}
Rental
/**
* 描述:获取常客积分
*/
public int getRenterPointer() {
return movie.getRenterPointer(getDaysRented());
}
影片的类型不同,但是只是以不同的方式回答相同的问题,这个有点像子类,这次我们可以将Movie建立3个子类,每个子类都有自己的计费方法:
这样我就可以使用多态来取代switch,但是这里有一个问题,一部影片在生命周期内可以修改自己的分类,一个对象却不能在生命周期内修改所属的class,可以利用State Pattern(模式)来表现不同的影片,这时就变成了:
这时的做法:
1、确保任何时候都能够通过setting和getting运用这些行为。
2、新建抽象类Price以及子类:
package com.xuzengqiang.ssb.movie;
/**
* 描述:价格抽象类
* @author xuzengqiang
* @since 2014-12-16 16:50:47
*/
public abstract class Price {
/**
* 描述:根据不同影片的类型获取价格代号
*/
abstract int getPriceCode();
}
ChildrensPrice
package com.xuzengqiang.ssb.movie;
/**
* 描述:儿童电影
* @author xuzengqiang
* @since 2014-12-16 16:52:32
*/
public class ChildrensPrice extends Price {
@Override
int getPriceCode() {
return Movie.CHILDRENS;
}
}
RegularPrice
package com.xuzengqiang.ssb.movie;
/**
* 描述:普通电影
* @author xuzengqiang
* @since 2014-12-16 16:52:32
*/
public class RegularPrice extends Price {
@Override
int getPriceCode() {
return Movie.REGULAR;
}
}
NewMoviePrice
package com.xuzengqiang.ssb.movie;
/**
* 描述:新片
* @author xuzengqiang
* @since 2014-12-16 16:52:32
*/
public class NewMoviePrice extends Price {
@Override
int getPriceCode() {
return Movie.NEW_MOVIE;
}
}
这个时候意味着我们在Movie中不是保存一个priceCode,而是一个Price对象。所以Movie可以修改成:(同时将getCharge方法移到Price中去)
package com.xuzengqiang.ssb.movie;
/**
* 影片实体
* @author xuzengqiang
* @since 2014-12-16 14:28:49
*/
public class Movie {
public static final int CHILDRENS = 0;
public static final int REGULAR = 1;
public static final int NEW_MOVIE = 2;
private String title;
/**
* 描述:代号
*/
private int priceCode;
/**
* 描述:价格对象
*/
private Price price;
public Movie(String title, int priceCode) {
this.title = title;
setPriceCode(priceCode);
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
/**
* 获取价格代号
* @return
*/
public int getPriceCode() {
return price.getPriceCode();
}
/**
* 这里不是保存的价格代号,而是生成价格对象
* @param priceCode
*/
public void setPriceCode(int priceCode) {
switch (priceCode) {
case CHILDRENS:
price = new ChildrensPrice();
break;
case REGULAR:
price = new RegularPrice();
break;
case NEW_MOVIE:
price = new NewMoviePrice();
break;
default:
throw new IllegalArgumentException("Incorrect Price Code");
}
}
/**
* 根据影片类型的不同获取费用
* @param daysRented:租赁时间
* @return
*/
public double getCharge(int daysRented) {
return price.getCharge(daysRented);
}
/**
* 描述:根据影片的类型以及租期获取常客积分
* @param daysRented
* @return
*/
public int getRenterPointer(int daysRented) {
if (getPriceCode() == Movie.NEW_MOVIE && daysRented > 1) {
return 2;
}
return 1;
}
}
Price
package com.xuzengqiang.ssb.movie;
/**
* 描述:价格抽象类
* @author xuzengqiang
* @since 2014-12-16 16:50:47
*/
public abstract class Price {
/**
* 描述:根据不同影片的类型获取价格代号
*/
abstract int getPriceCode();
/**
* 描述:根据价格代码的不同获取金额
* @param daysRented
* @return
*/
public double getCharge(int daysRented) {
double result = 0;
switch (getPriceCode()) {
// 如果是儿童片:前3天1.5,超过3天每天1.5
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3) {
result += (daysRented - 3) * 1.5;
}
break;
// 如果是普通片:不超过2天2块钱,超过2天每天1.5
case Movie.REGULAR:
result += 2;
if (daysRented > 2) {
result += (daysRented - 2) * 1.5;
}
break;
// 如果是新片:每天3块S
case Movie.NEW_MOVIE:
result += daysRented * 3;
break;
}
return result;
}
}
这样做仍然不能满足我们的要求,所以我们可以考虑在对应的子类中重写getCharge()方法,然后同样将Price()中的getCharge()方法定义为abstract的:
Price
package com.xuzengqiang.ssb.movie;
/**
* 描述:价格抽象类
* @author xuzengqiang
* @since 2014-12-16 16:50:47
*/
public abstract class Price {
/**
* 描述:根据不同影片的类型获取价格代号
*/
abstract int getPriceCode();
/**
* 描述:根据价格代码的不同获取金额
* @param daysRented
* @return
*/
abstract double getCharge(int daysRented);
}
ChildrensPrice
package com.xuzengqiang.ssb.movie;
/**
* 描述:儿童电影
* @author xuzengqiang
* @since 2014-12-16 16:52:32
*/
public class ChildrensPrice extends Price {
@Override
int getPriceCode() {
return Movie.CHILDRENS;
}
@Override
public double getCharge(int daysRented) {
double result = 1.5;
if (daysRented > 3) {
result += (daysRented - 3) * 1.5;
}
return result;
}
}
RegularPrice
package com.xuzengqiang.ssb.movie;
/**
* 描述:普通电影
* @author xuzengqiang
* @since 2014-12-16 16:52:32
*/
public class RegularPrice extends Price {
@Override
int getPriceCode() {
return Movie.REGULAR;
}
@Override
public double getCharge(int daysRented) {
double result = 2;
if (daysRented > 2) {
result += (daysRented - 2) * 1.5;
}
return result;
}
}
NewMoviePrice
package com.xuzengqiang.ssb.movie;
/**
* 描述:新片
* @author xuzengqiang
* @since 2014-12-16 16:52:32
*/
public class NewMoviePrice extends Price {
@Override
int getPriceCode() {
return Movie.NEW_MOVIE;
}
@Override
public double getCharge(int daysRented) {
return daysRented * 3;
}
}
同样的方法我们可以将Movie中的getRenterPointer()方法移动到Price中,只是这个时候我们不将Price中的getRenterPointer()方法设置为abstract,而是一种缺省的方式。
Movie
public int getRenterPointer(int daysRented) {
return price.getRenterPointer(daysRented);
}
Price:
/**
* 描述:根据影片的类型以及租期获取常客积分
* @param daysRented
* @return
*/
public int getRenterPointer(int daysRented) {
return 1;
}
这里的新片需要重写:
NewMoviePrice
@Override
public int getRenterPointer(int daysRented) {
return daysRented > 1 ? 2 : 1;
}
写到这里可以发现基本上可以满足后续的影片分类、或者修改费用规则、修改积分规则就很简单了