天天看點

《重構:改善既有代碼的設計》—第1章1.4節運用多态取代與價格相關的條件邏輯

本節書摘來自異步社群《重構:改善既有代碼的設計》一書中的第1章,第1.4節運用多态取代與價格相關的條件邏輯,作者【美】martin fowler,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

1.4 運用多态取代與價格相關的條件邏輯

這個問題的第一部分是switch語句。最好不要在另一個對象的屬性基礎上運用switch語句。如果不得不使用,也應該在對象自己的資料上使用,而不是在别人的資料上使用。

這暗示getcharge()應該移到movie類裡去:

為了讓它得以運作,我必須把租期長度作為參數傳遞進去。當然,租期長度來自rental對象。計算費用時需要兩項資料:租期長度和影片類型。為什麼我選擇将租期長度傳給movie對象,而不是将影片類型傳給rental對象呢?因為本系統可能發生的變化是加入新影片類型,這種變化帶有不穩定傾向。如果影片類型有所變化,我希望盡量控制它造成的影響,是以選擇在movie對象内計算費用。

我把上述計費方法放進movie類,然後修改rental的getcharge(),讓它使用這個新函數(圖1-12和圖1-13):

《重構:改善既有代碼的設計》—第1章1.4節運用多态取代與價格相關的條件邏輯
《重構:改善既有代碼的設計》—第1章1.4節運用多态取代與價格相關的條件邏輯

搬移getcharge()之後,我以相同手法處理常客積分計算。這樣我就把根據影片類型而變化的所有東西,都放到了影片類型所屬的類中。以下是重構前的代碼:

重構後的代碼如下:

終于……我們來到繼承

我們有數種影片類型,它們以不同的方式回答相同的問題。這聽起來很像子類的工作。我們可以建立movie的三個子類,每個都有自己的計費法(圖1-14)。

《重構:改善既有代碼的設計》—第1章1.4節運用多态取代與價格相關的條件邏輯

這麼一來,我就可以用多态來取代switch語句了。很遺憾的是這裡有個小問題,不能這麼幹。一部影片可以在生命周期内修改自己的分類,一個對象卻不能在生命周期内修改自己所屬的類。不過還是有一個解決方法:state模式[gang of four]。運用它之後,我們的類看起來像圖1-15。

《重構:改善既有代碼的設計》—第1章1.4節運用多态取代與價格相關的條件邏輯

加入這一層間接性,我們就可以在price對象内進行子類化動作[4],于是便可在任何必要時刻修改價格。

如果你很熟悉gof(gang of four,四巨頭)[5]所列的各種模式,可能會問:“這是一個state,還是一個strategy?”答案取決于price類究竟代表計費方式(此時我喜歡把它叫做pricer還pricingstrategy),還是代表影片的某個狀态(例如“star trek x是一部新片”)。在這個階段,對于模式(和其名稱)的選擇反映出你對結構的想法。此刻我把它視為影片的某種狀态。如果未來我覺得strategy能更好地說明我的意圖,我會再重構它,修改名字,以形成strategy。

為了引入state模式,我使用三個重構手法。首先運用replace type code with state/strategy (227),将與類型相關的行為搬移至state模式内。然後運用move method (142)将switch語句移到price類。最後運用replace conditional with polymorphism (255)去掉switch語句。

首先我要使用replace type code with state/strategy (227)。第一步驟是針對類型代碼使用self encapsulate field (171),確定任何時候都通過取值函數和設值函數來通路類型代碼。多數通路操作來自其他類,它們已經在使用取值函數。但構造函數仍然直接通路價格代碼[6]:

我可以用一個設值函數來代替:

然後編譯并測試,確定沒有破壞任何東西。現在我建立一個price類,并在其中提供類型相關的行為。為了實作這一點,我在price類内加入一個抽象函數,并在所有子類中加上對應的具體函數:

然後就可以編譯這些建立的類了。

現在,我需要修改movie類内的“價格代号”通路函數(取值函數/設值函數,如下),讓它們使用新類。下面是重構前的樣子:

這意味着我必須在movie類内儲存一個price對象,而不再是儲存一個_pricecode變量。此外我還需要修改通路函數:

現在我可以重新編譯并測試,那些比較複雜的函數根本不知道世界已經變了個樣兒。

現在我要對getcharge()實施move method (142)。下面是重構前的代碼:

搬移動作很簡單。下面是重構後的代碼:

搬移之後,我就可以開始運用replace conditional with polymorphism (255)了。

下面是重構前的代碼:

我的做法是一次取出一個case分支,在相應的類建立一個覆寫函數。先從regularprice開始:

這個函數覆寫了父類中的case語句,而我暫時還把後者留在原處不動。現在編譯并測試,然後取出下一個case分支,再編譯并測試。(為了保證被執行的确實是子類中的代碼,我喜歡故意丢一個錯誤進去,然後讓它運作,讓測試失敗。噢,我是不是有點太偏執了?)

處理完所有case分支之後,我就把price.getcharge()聲明為abstract:

現在我可以運用同樣手法處理getfrequentrenterpoints()。重構前的樣子如下[7]:

首先我把這個函數移到price類:

但是這一次我不把超類函數聲明為abstract。我隻是為新片類型增加一個覆寫函數,并在超類内留下一個已定義的函數,使它成為一種預設行為。

引入state模式花了我不少力氣,值得嗎?這麼做的收獲是:如果我要修改任何與價格有關的行為,或是添加新的定價标準,或是加入其他取決于價格的行為,程式的修改會容易得多。這個程式的其餘部分并不知道我運用了state模式。對于我目前擁有的這麼幾個小量行為來說,任何功能或特性上的修改也許都不合算,但如果在一個更複雜的系統中,有十多個與價格相關的函數,程式的修改難易度就會有很大的差別。以上所有修改都是小步驟進行,進度似乎太過緩慢,但是我一次都沒有打開過調試器,是以整個過程實際上很快就過去了。我寫本章文字所用的時間,遠比修改那些代碼的時間多得多。

現在我已經完成了第二個重要的重構行為。從此,修改影片分類結構,或是改變費用計算規則、改變常客積分計算規則,都容易多了。圖1-16和圖1-17描述state模式對于價格資訊所起的作用。

《重構:改善既有代碼的設計》—第1章1.4節運用多态取代與價格相關的條件邏輯
《重構:改善既有代碼的設計》—第1章1.4節運用多态取代與價格相關的條件邏輯