重构是在不改变软件可观察行为的前提下改善其内部结构。
设计模式为重构提供了目标
1.1 起点
这是一个影片出租店用的程序,计算每一个顾客的消费金额并打印详单。操作者告诉程序:租客租了哪些影片、租期多长,程序便根据租赁时间和影片类型算出费用。影片分为三类:普通片、儿童片、新片。除了计算费用,还要为常客计算积分,积分会根据租片种类是否为新片而有不同。
Movie(影片)- movie只是一个简单的纯数据类
/**
* 影片
* @author harry
*/
public class Movie {
// 影片类型
public static final int REGULAR = 0;//普通片
public static final int NEW_RELEASE = 1;//新片
public static final int CHILDRENS = 2;//儿童片
private String _title;
private int _priceCode;
public Movie(String _title, int _priceCode) {
super();
this._title = _title;
this._priceCode = _priceCode;
}
public String get_title() {
return _title;
}
public void set_title(String _title) {
this._title = _title;
}
public int get_priceCode() {
return _priceCode;
}
public void set_priceCode(int _priceCode) {
this._priceCode = _priceCode;
}
}
Rental(租赁)- rental表示某个顾客租了一部影片
/**
* 租赁实体
* @author harry
*/
public class Rental {
private Movie _movie;
private int _dayRented;
public Rental(Movie _movie, int _dayRented) {
super();
this._movie = _movie;
this._dayRented = _dayRented;
}
public Movie get_movie() {
return _movie;
}
public void set_movie(Movie _movie) {
this._movie = _movie;
}
public int get_dayRented() {
return _dayRented;
}
public void set_dayRented(int _dayRented) {
this._dayRented = _dayRented;
}
}
Customer(顾客)-customer表示顾客。与其他类一样,它也拥有数据和相应的访问函数
/**
* 顾客实体
* @author harry
*/
public class Customer {
private String _name;
private Vector<Rental> _rentals = new Vector<>();
public Customer(String _name) {
super();
this._name = _name;
}
public void addRental(Rental arg){
_rentals.addElement(arg);
}
public String getName() {
return _name;
}
public void setName(String _name) {
this._name = _name;
}
// 生成详情单
public String statement(){
double totalAmount = 0;//总金额
int frequentRenterPoints = 0;//本次总积分
Enumeration<Rental> rentals = _rentals.elements();
// 租赁备案
String result = "Rental Record for "+getName()+"\n";
while(rentals.hasMoreElements()){
double thisAmount = 0;
Rental each = rentals.nextElement();
// 计算金额
switch(each.get_movie().get_priceCode()){
case Movie.REGULAR:
thisAmount += 2;
if (each.get_dayRented() > 2) {
thisAmount += (each.get_dayRented()-2)*1.5;
}
break;
case Movie.NEW_RELEASE:
thisAmount += each.get_dayRented()*3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.get_dayRented() > 3) {
thisAmount += (each.get_dayRented()-3)*1.5;
}
break;
}
// 常规积分累加
frequentRenterPoints++;
// 特殊新书积分计算
if (each.get_movie().get_priceCode() == Movie.NEW_RELEASE &&
each.get_dayRented() > 1) {
frequentRenterPoints++;
}
// 显示凭条
result += "\t"+each.get_movie().get_title()+"\t"+String.valueOf(thisAmount)+"\n";
totalAmount += thisAmount;
}
// 组装页脚
result += "Amount owed is "+String.valueOf(totalAmount)+"\n";
result += "You earned "+String.valueOf(frequentRenterPoints)+" frequent renter points";
return result;
}
}
对起始程序的评价
Customer里的长长的statement()做的事情太多了,它做了原本应该由其他类完成的事情。
在这个例子中,如果用户希望以HTML格式输出详情单。上述程序中的statement()无法复用,唯一可以做就是编写一个全新的htmlStatement()。大量重复statement()的行为。当然这个还不是太费力,你可以把statement()复制一份按需求修改就可以了。
但是如果计费标准发生变化,又会如何呢?你必须同时修改statement()和htmlStatement(),并确保两处修改一致。当你后续还要修改的时候,复制粘贴带来的问题就会浮现出来。如果你编写的是一个永远不会修改的程序,那么剪剪粘粘就还好,但如果程序要保存很长时间,而且可能需要修改,复制粘贴行为就会造成潜在威胁。
现在第二个变化来了:用户希望改变影片分类规则,但是还没有决定怎么改。他们设想了几种方案,这些方案都会影响顾客消费和常客积分点的计算方式,作为一个经验丰富的开发者,你可以肯定:不论用户提出什么方案,你唯一能够获得的保证就是他们一定会在六个月之内再次修改它。
为了应付分类规则和计费规则的变化,程序必须对statement()做出修改,但如果我们把statement()内的代码复制到用以打印HTML详单的函数中,就必须确保将来在任何修改这两个地方保持一致,随着各种规则变的越来越复杂,适当的修改点越来越难,不犯错的机会越来越少。
接下来重构技术就该粉墨登场了。