閱讀《重構(gòu)》之一:重要的第一個例子

第一個例子很重要,因為它通過實際操作帶你走進什么是重構(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)

movie繼承.JPG

然后是使用state模式,修改后的類圖:

state模式movie繼承.JPG

可以看到是: 把價格計算單獨抽離做了新的類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)進行不同方式價格計算。

最終類圖.JPG
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容