### 起點
實例非常簡單。這是一個影片出租店用的程序,計算每一位顧客的消費金額并打印報表(statement)。操作者告訴程序:顧客租了哪些影片、租期多長,程序便根據租賃時間和影片類型算出費用。影片分為三類:普通片、兒童片和新片。除了計算費用,還要為常客計算點數;點數會隨著「租片種類是否為新片」而有不同。
我以數個classes 表現這個例子中的元素。圖1.1是一張UML class diagram(類圖),用以顯示這些classes 。我會逐一列出這些classes 的代碼。

圖1.1 本例一開始的各個classes 。此圖只顯示最重要的特性。圖中所用符號是UML(Unified Modeling Language ,統一建模語言,[Fowler, UML])。
**Movie(影片)**
Movie只是一個簡單的data class(純數據類)。
~~~
public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String _title; //名稱
private int _priceCode; //價格(代號)
public Movie(String title, int priceCode) {
_title = title;
_priceCode = priceCode;
}
public int getPriceCode() {
return _priceCode;
}
public void setPriceCode(int arg) {
_priceCode = arg;
}
public String getTitle (){
return _title;
};
}
~~~
**Rental(租賃)**
Rental class 表示「某個顧客租了一部影片」。
~~~
class Rental {
private Movie _movie; //影片
private int _daysRented; //租期
public Rental(Movie movie, int daysRented) {
_movie = movie;
_daysRented = daysRented;
}
public int getDaysRented() {
return _daysRented;
}
public Movie getMovie() {
return _movie;
}
}
~~~
譯注:中文版(本書)支持網站提供本章重構過程中的各階段完整代碼(共分七個階段),并含測試。網址見于封底。
**Customer(顧客)**
Customer class 用來表示顧客。就像其他classes一樣,它也擁有數據和相應的訪問函數(accessor):
~~~
class Customer {
private String _name; //姓名
private Vector _rentals = new Vector(); //租借記。
public Customer (String name){
_name = name;
};
public void addRental(Rental arg) {
_rentals.addElement(arg);
}
public String getName (){
return _name;
};
//譯注:續下頁...
~~~
Customer「還提供了一個用以制造報表的函數(method),圖1.2顯示這個函數帶來的交互過程(interactions )。完整代碼顯示于下一頁。

圖1.2 statement() 的交互過程(interactions 。
~~~
public String statement() {
double totalAmount = 0; //總消費金。
int frequentRenterPoints = 0; //常客積點
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) rentals.nextElement(); //取得一筆租借記。
//determine amounts for each line
switch (each.getMovie().getPriceCode()) { //取得影片出租價格
case Movie.REGULAR: //普通片
thisAmount += 2;
if (each.getDaysRented() > 2)
thisAmount += (each.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE: //新片
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS: //兒童。
thisAmount += 1.5;
if (each.getDaysRented() > 3)
thisAmount += (each.getDaysRented() - 3) * 1.5;
break;
}
// add frequent renter points (累計常客積點。
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
each.getDaysRented() > 1) frequentRenterPoints ++;
//show figures for this rental(顯示此筆租借記錄)
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
//add footer lines(結尾打印)
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}
~~~
**對此起始程序的評。**
這個起始程序給你留下什么印象?我會說它設計得不好,而且很明顯不符合面向對象精神。對于這樣一個小程序,這些缺點其實沒有什么關系。快速而隨性(quick and dirty )地設計一個簡單的程序并沒有錯。但如果這是復雜系統中具有代表性的一段, 那么我就真的要對這個程序信心動搖了。Customer 里頭那個長長的statement() 做的事情實在太多了,它做了很多原本應該由其他完成的事情。
即便如此,這個程序還是能正常工作。所以這只是美學意義上的判斷,只是對丑陋代碼的厭惡,是嗎?在我們修改這個系統之前的確如此。編譯器才不會在乎代碼好不好看呢。但是當我們打算修改系統的時候,就涉及到了人,而人在乎這些。差勁的系統是很難修改的,因為很難找到修改點。如果很難找到修改點,程序員就很有可能犯錯,從而引入「臭蟲」(bugs)。
在這個例子里,我們的用戶希望對系統做一點修改。首先他們希望以HTML 格式打印報表,這樣就可以直接在網頁上顯示,這非常符合潮流。現在請你想一想,這個變化會帶來什么影響。看看代碼你就會發現,根本不可能在打印報表的函數中復用(reuse)目前statement() 的任何行為。你惟一可以做的就是編寫一個全新的htmlStatement() ,大量重復statement() 的行為。當然,現在做這個還不太費力,你可以把statement() 復制一份然后按需要修改就是。
但如果計費標準發生變化,又會發生什么事?你必須同時修改statement() 和htmlstatement() ,并確保兩處修改的一致性。當你后續還要再修改時,剪貼(copy-paste)問題就浮現出來了。如果你編寫的是一個永不需要修改的程序,那么剪剪貼貼就還好,但如果程序要保存很長時間,而且可能需要修改,剪貼行為就會造成潛在的威脅。
現在,第二個變化來了:用戶希望改變影片分類規則,但是還沒有決定怎么改。他 們設想了幾種方案,這些方案都會影響顧客消費和常客積點的計算方式。作為一個經驗豐富的開發者,你可以肯定:不論用戶提出什么方案,你惟一能夠獲得的保證就是他們一定會在六個月之內再次修改它。
為了應付分類規則和計費規則的變化,程序必須對statement() 作出修改。但如果我們把statement() 內的代碼拷貝到用以打印報表的函數中,我們就必須確保將來的任何修改在兩個地方保持一致。隨著各種規則變得愈來愈復雜,適當的修改點愈來愈難找,不犯錯的機會也愈來愈少。
你的態度也許傾向于「盡量少修改程序」:不管怎么說,它還運行得很好。你心里頭牢牢記著那句古老的工程學格言:「如果它沒壞,就別動它」。這個程序也許還沒壞掉,但它帶來了傷害。它讓你的生活比較難過,因為你發現很難完成客戶所需的修改。這時候就該重構技術粉墨登場了。
TIP:如果你發現自己需要為程序添加一個特性,而代碼結構使你無法很方便地那么做,那就先重構那個程序,使特性的添加比較容易進行,然后再添加特性。
- 譯序 by 侯捷
- 譯序 by 熊節
- 序言
- 前言
- 章節一 重構,第一個案例
- 起點
- 重構的第一步
- 分解并重組statement()
- 運用多態(Polymorphism)取代與價格相關的條件邏輯
- 結語
- 章節二 重構原則
- 何謂重構
- 為何重構
- 「重構」助你找到臭蟲(bugs)
- 何時重構
- 怎么對經理說?
- 重構的難題
- 重構與設計
- 重構與性能(Performance)
- 重構起源何處?
- 章節三 代碼的壞味道
- Duplicated Code(重復的代碼)
- Long Method(過長函數)
- Large Class(過大類)
- Long Parameter List(過長參數列)
- Divergent Change(發散式變化)
- Shotgun Surgery(散彈式修改)
- Feature Envy(依戀情結)
- Data Clumps(數據泥團)
- Primitive Obsession(基本型別偏執)
- Switch Statements(switch驚悚現身)
- Parallel Inheritance Hierarchies(平行繼承體系)
- Lazy Class(冗贅類)
- Speculative Generality(夸夸其談未來性)
- Temporary Field(令人迷惑的暫時值域)
- Message Chains(過度耦合的消息鏈)
- Middle Man(中間轉手人)
- Inappropriate Intimacy(狎昵關系)
- Alternative Classes with Different Interfaces(異曲同工的類)
- Incomplete Library Class(不完美的程序庫類)
- Data Class(純稚的數據類)
- Refused Bequest(被拒絕的遺贈)
- Comments(過多的注釋)
- 章節四 構筑測試體系
- 自我測試代碼的價值
- JUnit測試框架
- 添加更多測試
- 章節五 重構名錄
- 重構的記錄格式
- 尋找引用點
- 這些重構準則有多成熟
- 章節六 重新組織你的函數
- Extract Method(提煉函數)
- Inline Method(將函數內聯化)
- Inline Temp(將臨時變量內聯化)
- Replace Temp with Query(以查詢取代臨時變量)
- Introduce Explaining Variable(引入解釋性變量)
- Split Temporary Variable(剖解臨時變量)
- Remove Assignments to Parameters(移除對參數的賦值動作)
- Replace Method with Method Object(以函數對象取代函數)
- Substitute Algorithm(替換你的算法)
- 章節七 在對象之間搬移特性
- Move Method(搬移函數)
- Move Field(搬移值域)
- Extract Class(提煉類)
- Inline Class(將類內聯化)
- Hide Delegate(隱藏「委托關系」)
- Remove Middle Man(移除中間人)
- Introduce Foreign Method(引入外加函數)
- Introduce Local Extension(引入本地擴展)
- 章節八 重新組織數據
- Self Encapsulate Field(自封裝值域)
- Replace Data Value with Object(以對象取代數據值)
- Change Value to Reference(將實值對象改為引用對象)
- Replace Array with Object(以對象取代數組)
- Replace Array with Object(以對象取代數組)
- Duplicate Observed Data(復制「被監視數據」)
- Change Unidirectional Association to Bidirectional(將單向關聯改為雙向)
- Change Bidirectional Association to Unidirectional(將雙向關聯改為單向)
- Replace Magic Number with Symbolic Constant(以符號常量/字面常量取代魔法數)
- Encapsulate Field(封裝值域)
- Encapsulate Collection(封裝群集)
- Replace Record with Data Class(以數據類取代記錄)
- Replace Type Code with Class(以類取代型別碼)
- Replace Type Code with Subclasses(以子類取代型別碼)
- Replace Type Code with State/Strategy(以State/strategy 取代型別碼)
- Replace Subclass with Fields(以值域取代子類)
- 章節九 簡化條件表達式
- Decompose Conditional(分解條件式)
- Consolidate Conditional Expression(合并條件式)
- Consolidate Duplicate Conditional Fragments(合并重復的條件片段)
- Remove Control Flag(移除控制標記)
- Replace Nested Conditional with Guard Clauses(以衛語句取代嵌套條件式)
- Replace Conditional with Polymorphism(以多態取代條件式)
- Introduce Null Object(引入Null 對象)
- Introduce Assertion(引入斷言)
- 章節十一 處理概括關系
- Pull Up Field(值域上移)
- Pull Up Method(函數上移)
- Pull Up Constructor Body(構造函數本體上移)
- Push Down Method(函數下移)
- Push Down Field(值域下移)
- Extract Subclass(提煉子類)
- Extract Superclass(提煉超類)
- Extract Interface(提煉接口)
- Collapse Hierarchy(折疊繼承關系)
- Form Template Method(塑造模板函數)
- Replace Inheritance with Delegation(以委托取代繼承)
- Replace Delegation with Inheritance(以繼承取代委托)
- 章節十二 大型重構
- 這場游戲的本質
- Tease Apart Inheritance(梳理并分解繼承體系)
- Convert Procedural Design to Objects(將過程化設計轉化為對象設計)
- Separate Domain from Presentation(將領域和表述/顯示分離)
- Extract Hierarchy(提煉繼承體系)
- 章節十三 重構,復用與現實
- 現實的檢驗
- 為什么開發者不愿意重構他們的程序?
- 現實的檢驗(再論)
- 重構的資源和參考資料
- 從重構聯想到軟件復用和技術傳播
- 結語
- 參考文獻
- 章節十四 重構工具
- 使用工具進行重構
- 重構工具的技術標準(Technical Criteria )
- 重構工具的實用標準(Practical Criteria )
- 小結
- 章節十五 集成
- 參考書目