### 運用多態(Polymorphism)取代與價格相關的條件邏輯
這個問題的第一部分是switch 語句。在另一個對象的屬性(attribute)基礎上運用switch 語句,并不是什么好主意。如果不得不使用,也應該在對象自己的數據上使用,而不是在別人的數據上使用。
~~~
class Rental...
double getCharge() {
double result = 0;
switch (getMovie().getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (getDaysRented() > 2)
result += (getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += getDaysRented() * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (getDaysRented() > 3)
result += (getDaysRented() - 3) * 1.5;
break;
}
return result;
}
~~~
這暗示getCharge() 應該移到Movie class 里頭去:
~~~
class Movie...
double getCharge(int daysRented) {
double result = 0;
switch (getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) * 1.5;
break;
}
return result;
}
~~~
為了讓它得以運作,我必須把「租期長度」作為參數傳遞進去。當然,「租期長度」來自收Rental 對象。計算費用時需要兩份數據:「租期長度」和「影片類型」。為什么我選擇「將租期長度傳給Movie 對象」而不是「將影片類型傳給Rental 對象」呢?因為本系統可能發生的變化是加入新影片類型,這種變化帶有不穩定傾向。如果影片類型有所變化,我希望掀起最小的鏈滴,所以我選擇在Movie 對象內計算費用。
我把上述計費方法放進Movie class 里頭,然后修改Rental 的getCharge(),讓它使用這個新函數(圖1.12和圖1.13):

圖1.12 本節所討論的兩個函數被移到Movie class 內之前系統的class diagram

圖1.13 本節所討論的兩個函數被移到Movie class 內之后系統的class diagram
~~~
class Rental...
double getCharge() {
return _movie.getCharge(_daysRented);
}
~~~
搬移getCharge() 之后,我以相同手法處理常客積點計算。這樣我就把根據影片類型而變化的所有東西,都放到了影片類型所屬的class 中。
以下是重構前的代碼:
~~~
class Rental...
int getFrequentRenterPoints() {
if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1)
return 2;
else
return 1;
}
~~~
重構如下:
~~~
Class rental...
int getFrequentRenterPoints() {
return _movie.getFrequentRenterPoints(_daysRented);
}
class movie...
int getFrequentRenterPoints(int daysRented) {
if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
return 2;
else
return 1;
}
~~~
終于……我們來到繼承(Inheritance)
我們有數種影片類型,它們以不同的方式回答相同的問題。這聽起來很像subclasses 的工作。我們可以建立Movie 的三個subclasses ,每個都有自己的計費法(圖1.14)。

圖1.14 以繼承機制表現不同的影片類型
這么一來我就可以運用多態(polymorphism)來取代switch 語句了。很遺憾的是這里有個小問題,不能這么干。一部影片可以在生命周期內修改自己的分類,一個對象卻不能在生命周期內修改自己所屬的class。不過還是有一個解決方法:State pattern(模式)[Gang of Four]。運用它之后,我們的classes 看起來像圖1.15。

圖1.15 運用State pattern(模式)表現不同的影片
加入這一層間接性,我們就可以在Price 對象內進行subclassing 動作(譯注:一如圖1.15),于是便可在任何必要時刻修改價格。
如果你很熟悉Gang of Four 所列的各種模式(patterns),你可能會問:『這是一個State 還是一個Strategy?』答案取決于Price class 究竟代表計費方式(此時我喜歡把它叫做Pricer 或PricingStrategy),或是代表影片的某個狀態(state,例如「Star Trek X 是一部新片」)。在這個階段,對于模式(和其名稱)的選擇反映出你對結構的想法。此刻我把它視為影片的某種狀態(state)。如果未來我覺得Strategy 能更好地說明我的意圖,我會再重構它,修改名字,以形成Strategy 。
為了引入State 模式,我使用三個重構準則。首先運用Replace Type Code with State/Strategy,將「與型別相依的行為」(type code behavior )搬移至State 模式內。然后運用Move Method 將switch 語句移到Price class 里頭。最后運用Replace Conditional with Polymorphism去掉switch 語句。
首先我要使用Replace Type Code with State/Strategy。第一步驟是針對「與 型別相依的行為」使用Self Encapsulate Field,確保任何時候都通過getting 和setting 兩個函數來運用這些行為。由于多數代碼來自其他classes,所以多數函數都己經使用getting 函數。但構造函數(constructor )仍然直接訪問價格代號(譯注:程序中的_priceCode):
~~~
class Movie...
public Movie(String name, int priceCode) {
_name = name;
_priceCode = priceCode;
}
~~~
我可以用一個setting 函數來代替:
~~~
class Movie
public Movie(String name, int priceCode) {
_name = name;
setPriceCode(priceCode); //譯注:這就是一個set method
}
~~~
然后編譯并測試,確保沒有破壞任何東西。現在我加入新class,并在Price 對象中提供「與型別相依的行為」。為了實現這一點,我在Price 內加入一個抽象函數(abstract method ),并在其所有subclasses 中加上對應的具體函數(concrete method):
~~~
abstract class Price {
abstract int getPriceCode(); //取得價格代號
}
class ChildrensPrice extends Price {
int getPriceCode() {
return Movie.CHILDRENS;
}
}
class NewReleasePrice extends Price {
int getPriceCode() {
return Movie.NEW_RELEASE;
}
}
class RegularPrice extends Price {
int getPriceCode() {
return Movie.REGULAR;
}
}
~~~
現在我可以編譯這些新classes了。
現在,我需要修改Movie class 內的「價格代號」訪問函數(get/set函數,如下),讓它們使用新class。
下面是重構前的樣子:
~~~
public int getPriceCode() {
return _priceCode;
}
public setPriceCode (int arg) {
_priceCode = arg;
}
private int _priceCode;
~~~
這意味我必須在Movie class 內保存一個Price 對象,而不再是保存一個_priceCode 變量。此外我還需要修改訪問函數(譯注:即get/set函數):
~~~
class Movie...
public int getPriceCode() { //取得價格代號
return _price.getPriceCode();
}
public void setPriceCode(int arg) { //設定價格代號
switch (arg) {
case REGULAR:
_price = new RegularPrice();
break;
case CHILDRENS:
_price = new ChildrensPrice();
break;
case NEW_RELEASE:
_price = new NewReleasePrice();
break;
default:
throw new IllegalArgumentException("Incorrect Price Code");
}
}
private Price _price;
~~~
現在我可以重新編譯并測試,那些比較復雜的函數根本不知道世界巳經變了個樣兒。
現在我要對getCharge() 實施Move Method。
下面是重構前的代碼:
~~~
class Movie...
double getCharge(int daysRented) {
double result = 0;
switch (getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) * 1.5;
break;
}
return result;
}
~~~
搬移動作很簡單。下面是重構后的代碼。
~~~
class Movie...
double getCharge(int daysRented) {
return _price.getCharge(daysRented);
}
class Price...
double getCharge(int daysRented) {
double result = 0;
switch (getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) * 1.5;
break;
}
return result;
}
~~~
搬移之后,我就可以開始運用Replace Conditional with Polymorphism了。下面是重構前的代碼。
~~~
class Price...
double getCharge(int daysRented) {
double result = 0;
switch (getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) * 1.5;
break;
}
return result;
}
~~~
我的作法是一次取出一個case 分支,在相應的class內建立一個覆寫函數(overriding method)。先從RegularPrice 開始:
~~~
class RegularPrice...
double getCharge(int daysRented){
double result = 2;
if (daysRented > 2)
result += (daysRented - 2) * 1.5;
return result;
}
~~~
這個函數覆寫(overrides)了父類中的case 語句,而我暫時還把后者留在原處不動。現在編譯并測試,然后取出下一個case 分支,再編譯并測試。(為了保證被執行的的確是subclass 代碼,我喜歡故意丟一個錯誤進去,然后讓它運行,讓測試失敗。噢,我是不是有點太偏執了?)
~~~
class ChildrensPrice
double getCharge(int daysRented){
double result = 1.5;
if (daysRented > 3)
result += (daysRented - 3) * 1.5;
return result;
}
class NewReleasePrice...
double getCharge(int daysRented){
return daysRented * 3;
}
~~~
處理完所有case 分支之后,我就把Price.getCharge() 聲明為abstract。
~~~
class Price...
abstract double getCharge(int daysRented);
~~~
現在我可以運用同樣手法處理getFrequentRenterPoints()。重構前的樣子如下(譯注:其中有「與型別相依的行為」,也就是「判斷是否為新片」那個動作)。
~~~
class Rental...
int getFrequentRenterPoints(int daysRented) {
if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
return 2;
else
return 1;
}
~~~
首先我把這個函數移到Price class 里頭。
~~~
Class Movie...
int getFrequentRenterPoints(int daysRented) {
return _price.getFrequentRenterPoints(daysRented);
}
Class Price...
int getFrequentRenterPoints(int daysRented) {
if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
return 2;
else
return 1;
}
~~~
但是這一次我不把superclass 函數聲明為abstract。我只是為「新片類型」產生一個覆寫函數(overriding method ),并在superclass 內留下一個已定義的函數,使它成為一種缺省行為。
~~~
//譯注:在新片中產生一個覆寫函數(overriding method )
Class NewReleasePrice
int getFrequentRenterPoints(int daysRented) {
return (daysRented > 1) ? 2: 1;
}
//譯注:在superclass 內保留它,使它成為一種缺省行。
Class Price...
int getFrequentRenterPoints(int daysRented){
return 1;
}
~~~
引入State 模式花了我不少力氣,值得嗎?這么做的收獲是:如果我要修改任何與價格有關的行為,或是添加新的定價標準,或是加入其他取決于價格的行為,程序的修改會容易得多。這個程序的其余部分并不知道我運用了State 模式。對于我目前擁有的這么幾個小量行為來說,任何功能或特性上的修改也許都稱不上什么困難,但如果在一個更復雜的系統中,有十多個與價格相關的函數,程序的修改難易度就會有很大的區別。以上所有修改都是小步驟進行,進度似乎太過緩慢,但是沒有任何一次我需要打開調試器(debugger),所以整個過程實際上很快就過去了。我書寫本章所用的時間,遠比修改那些代碼的時間多太多了。
現在我已經完成了第二個重要的重構行為。從此,修改「影片分類結構」,或是改變「費用計算規則」、改變常客積點計算規則,都容易多了。圖1.16和圖1.17描述State 模式對于價格信息所起的作用。

圖1.16 運用State pattern (模式)當時的Interaction diagram

圖1.17 加入State pattern (模式)之后的class diagram
- 譯序 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 )
- 小結
- 章節十五 集成
- 參考書目