第一個例子很重要,因為它通過實際操作帶你走進什么是重構(gòu),為何重構(gòu)可以帶來實用的價值。作者也在書開頭說了,理論容易讓他昏昏入睡,一個好的例子能帶來更好的理解,他做到了。
這是一個什么例子?
是一個用戶租聘錄像帶的小程序,包含3個類:用戶、租聘和錄像帶,它們的關(guān)系如下:customer --> rental --> movie。然后核心的業(yè)務(wù)是計算用戶租聘的價格和加分,并生成訂單輸出。
最初始的代碼是把這個生成訂單的邏輯全部集中在一個statement
函數(shù)里,內(nèi)容大致如下:
public String statement(){
double totalAmount = 0; //此次租聘總價
int rentalPoints = 0; //此次租聘積分
String result = "Rental Record for xxx";//租聘訂單內(nèi)容
for(each in rentals){ //(1)
double thisAmount = 0;
switch(each.getMovie().getPriceCode()){
case xxx
case xxx
//根據(jù)影片類型計算價格
thisAmount = xxx
}
//根據(jù)類型計算用戶積分(2)
rentalPoints += xxx
//添加這個影片內(nèi)容輸入到訂單(3)
result += xxx
}
}
首先這個程序并沒有什么問題,看上去,但是為了讓程序更健壯、更容易應(yīng)對變化(這也是重構(gòu)的最重要的目的之一),我們需要對未來可能的改變做一些假設(shè)判斷。
首先分析一個整個業(yè)務(wù),主要的內(nèi)容為:
- 計算價格
- 計算積分
- 生成訂單內(nèi)容,目前是純文字類型。
那么可能的改變就有:
- 來了新類型影片,價格和積分計算都不同已有類型
- 已有類型的計算方式發(fā)生變化,比如店開不下去了,或者臨時促銷等等
- 改用HTML方式輸出訂單,甚至改成生成圖片發(fā)送給用戶等等。
這些都會導(dǎo)致statement
這個函數(shù)的改變,然而它們卻是不同動機引發(fā)的。為了讓修改集中在更小的邏輯范圍里,需要對statement
進行拆分。
改進1
把對影片價格的計算移動到單獨的函數(shù)里。也就是(1)位置switch部分。這一步就可以應(yīng)對增加新類型或者舊類型價格計算方式改變。這些改變都會在單獨的新函數(shù)里修改,而不會干擾到statement
函數(shù)。
改進2
當(dāng)把影片價格的計算移動到單獨函數(shù)去之后,會發(fā)現(xiàn)這個函數(shù)并沒有用到當(dāng)前類customer的任何信息,它的邏輯完全是依賴于rental這個類的(一個rental代表一個影片的租聘,和影片是一對一的關(guān)系)。
這也是書里提及的最重要的重構(gòu)標(biāo)識之一:當(dāng)一個函數(shù)更多的依賴于另一個類而不是當(dāng)前類的時候,應(yīng)該考慮把這個函數(shù)移動到那個依賴更多的類中。
所以在Rental
類中添加double getCharge()
函數(shù),這樣最開始的switch部分就改為了:
thisAmount = each.getCharge()
.
做完這一步,那么影片類型和價格計算的變化,不僅不會影響到statement
函數(shù),甚至不會影響到customer
這個類。
改進3
積分計算和上面價格計算一樣,也拆分到Rental
類里面去。
改進4
書里接下來的改進是把statement
函數(shù)里的循環(huán)都拆了,循環(huán)存在的目的是為計算總體的價格和積分,總價格計算放到一個新函數(shù)getTotalCharge
里,總積分計算方法新函數(shù)getTotalFrequentRentalPoints
里。
書里給的理由是減少臨時變量,但我覺得不是重點,結(jié)合后面(p32)的這一句話:如果沒有這些查詢函數(shù),其他函數(shù)就必須了解Rental類,并自行建立循環(huán)。這里有幾點非常重要:
首先根本的原因是需求。其他地方也需要總價格、總積分這些,比如你的程序有5個地方用到積分計算,而他們需要的都是總積分,那么一個單獨的用來計算總積分的函數(shù)就變得非常需要。一個函數(shù)要還是不要,關(guān)鍵看需求。
我一直認為模塊封裝就要像黑盒子一樣。你提供了需要的函數(shù),滿足外界需要的任何需求,那么外界就不需要了解你的內(nèi)部,對方也就能夠安心的干自己的事。如果一個小需求需要你把整個程序的源碼全部讀一遍,那肯定是又浪費時間,又很容易干擾到其他部分。
這一次改進后,任何其他地方需要用到總價格或總積分,它不需要知道任何細節(jié),到底是循環(huán)還是不循環(huán),各種價格如何計算或者哪些類型影片不計入積分等等,它只需要調(diào)用getTotalCharge
,一切ok!
改進5
從改進2那里可知,其實仔細想,價格和積分的計算更多依賴于movie類,對rental的依賴只有租聘的天數(shù),所以把它們進一步移動到movie才是對的。如rental類變?yōu)椋?/p>
class Rental
double getCharge(){
return _movie.getCharge(_daysRented);
}
改進6
接下來有兩點改進:1. 引入state模式 2.使用多態(tài)代替switch.
什么是使用多態(tài)代替繼承?
首先多態(tài)是obj.method1()
會因為obj的類型不同而調(diào)用不同的方法,在計算影片的價格和積分時,同樣因為影片類型不同而進行不同的操作,這正好符合多態(tài)的行為方式。
修改之前是:
class Movie
double getCharge(){
switch(type){
case 1:
xxx
break;
case 2:
xxx
break;
.....
}
}
修改之后變?yōu)椋?/p>
class Type1Movie
double getCharge(){
type1的計算方式
}
class Type2Movie
double getCharge(){
type2的計算方式
}
......
不同的計算方式分散到不同的子類里去了,而外界調(diào)用的時候卻沒有改變,還是movieObj.getCharge()
。
這種手段可以很好的應(yīng)對新增或刪除類型,新增類型只需要添加一個新類,實現(xiàn)getCharge
方法,就一切正常運轉(zhuǎn)了,原有的類甚至不知道新增或者刪除了一個類。
這樣就有了下圖的結(jié)構(gòu)
然后是使用state模式,修改后的類圖:
可以看到是: 把價格計算單獨抽離做了新的類price,然后price根據(jù)不同計算方式構(gòu)建繼承體系。
state模式是設(shè)計模式那本書里提的,我的理解是:類的行為受到某個屬性的影響,當(dāng)這個影響變得復(fù)雜之后,比如要做許多的判斷,可以把這個屬性抽離作為狀態(tài)類,把相關(guān)的行為搬移到狀態(tài)類里。
其實從這里可以看出,繼承也是可以達到減輕狀態(tài)判斷的,那么state模式的意義何在?這里有一個問答,雖然問的是繼承和strategy模式的區(qū)別,但也可以理解到state模式上。簡單說,如果一個類,有多種影響行為的屬性,全部繼承,那么子類數(shù)量將為相當(dāng)巨大。比如屬性1有4種狀態(tài),屬性2有5種狀態(tài),那么子類就有20個了。而采用state模式,可以讓各種state自由組合,更方便。
書里提到使用state模式的只有一句話:一步影片可以在生命周期內(nèi)修改自己的分類,一個對象卻不能在生命周期內(nèi)修改自己所屬的類。就是說影片的類型在邏輯上是變化的,而如果使用類繼承策略,那么某個影片對象會因為無法修改自己的類而無法修改影片類型。
使用繼承體系,那么邏輯上的類型就和程序里的類綁定了,而如果邏輯上是可變的,那么就產(chǎn)生了沖突。
而采用state模式,就可以化解這個問題,只需切換不同的屬性對象,就擁有了不同的類型。就像。。。自行車裝上了電動馬達就變成了電動車了。
最后:movie對象擁有price對象,price根據(jù)計算方式拆分不同子類,使用多態(tài)進行不同方式價格計算。