重構是在不改變軟件可觀察行爲的前提下改善其內部結構。
設計模式爲重構提供了目標
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詳單的函數中,就必須確保將來在任何修改這兩個地方保持一致,隨着各種規則變的越來越複雜,適當的修改點越來越難,不犯錯的機會越來越少。
接下來重構技術就該粉墨登場了。