1. 程式人生 > >重構-改善既有程式碼的設計:處理概括關係 (九)

重構-改善既有程式碼的設計:處理概括關係 (九)

簡化函式呼叫

1.  Pull Up Field 欄位上移

兩個子類擁有相同的欄位。將該欄位移至超類。


如果各子類是分別開發的,或者是在重構過程中組合起來的,你常會發現它們擁有重複特性,特別是欄位更容易重複。這樣的欄位有時擁有相似的名字,但也並非絕對如此。判斷若干欄位是否重複,唯一的辦法就是觀察函式如何使用它們。如果它們被使用的方式很相似,你就可以將它們歸納到超類去。

2.  Pull up Method 函式上移

有些函式,在各個子類中產生完全相同的結果。將該函式移至超類。


避免重複行為是很重要的。儘管重複的2個函式也可以各自工作的很好,但重複自身只會成為錯誤的滋生地,此外別無價值。無論何時,只要系統內出現重複,你就面臨“修改其中一個卻未能修改另一個”的風險。通常,找出重複也有一定困難。

       如果某個函式在各子類中的函式體相同,這就是做顯而易見的Pull Up Method (方法上移)適用場合。當然,情況並不總是如此明顯。你也可以只管放心重構,再看看測試程式會不會發牢騷,但這就需要對你的測試有充分的信心。觀察這些可能重複的函式之間的差異往往大有收穫:它們經常會展示那些忘記測試的行為。

       Pull Up Method (方法上移)常常緊隨其他重構而被使用。也許你能找出若干個身處不同子類的函式,而它們又可以通過某種形式的引數調整成為相同的函式。這時候,最簡單的辦法就是首先分別調整這些函式的引數,然後再將它們概況到超類中。當然,如果你足夠自信,也可以一次完成這2個步驟。

       有一種特殊情況也需要使用Pull Up Method (方法上移):子類的函式覆寫了超類的函式,但仍然做相同的工作。

       Pull Up Method (方法上移)過程中最麻煩的一點就是:被提升的函式可能會引用只出現於子類而不出現於超類的特性。如果被引用的是個函式,你可以將該函式也一同提升到超類,或者在超類中建立一個抽象函式。在此過程中,你可能需要修改某個函式的簽名,或建立一個委託函式。

       如果2個函式相似但不相同,你或許可以先借助Form Template Method (塑造模板函式)構造出相同的函式,然後再提升它們


3.  Pull up Constructor Body 建構函式本體上移

你在各個子類中擁有一些建構函式,它們的本體幾乎完全一致。在超類中新建一個建構函式,並在子類建構函式中呼叫它。


建構函式是很奇妙的東西。它們不是普通函式,使用它們比使用普通函式受到更多的限制。

如果你看見各個子類中的函式有共同的行為,第一個念頭應該是將共同行為提煉到一個獨立函式中,然後將這個函式提升到超類。對建構函式而言,它們彼此的共同行為往往就是“物件的構建”。這時候你需要在超類中提供一個建構函式,然後讓子類都來呼叫它。很多時候,子類建構函式的唯一動作就是呼叫超類建構函式。這裡不能運用 Pull Up Method (方法上移),因為你無法在子類中繼承超類建構函式。

如果重構過程過於複雜,你可以考慮使用 Replace Constructor with Factory Method (以工廠函式取代建構函式)。



4.  Push down Method 函式下移

超類中的某個函式只與部分子類有關。將這個函式移到相關的那些子類去。


push down Method (函式下移)和Pull Up Method (函式上移)恰恰相反,當有必要把某些行為從超類移至特定的子類時,就使用push down Method (函式下移),它通常也只在這種時候使用。使用Extract Subclass (提煉子類)之後可能會需要它.


5.  Push down Fiedld 欄位下移

超類中的某個欄位只被部分子類用到,將這個欄位移到需要它的那些子類去


如果只有某些子類需要超類內的一個欄位,那就可以使用本項重構。


6.  Extract Subclass 提煉子類

類中的某些特性只被某些例項用到。新建一個子類,將上面所說的那一部分特性移到子類中。


     使用Extract Subclass (提煉子類)的主要動機是:你發現類中的某些行為只被一部分例項用到,其他例項不需要它們。有時候這種行為上的差異是通過型別碼區分的,此時你可以使用Replace Type Code with Subclass (以子類取代型別碼)或Replace Type Code with State/Strategy (以狀態策略取代型別碼)。但是,並非一定要出現了型別碼才表示需要考慮使用子類。

       Extract Class (提煉類)是Extract Subclass (提煉子類)之外的另一個選擇,2者之間的抉擇其實就是委託和繼承之間的抉擇。Extract Subclass (提煉子類)通常更容易進行,但它也有限制:一旦物件建立完成你無法再改變與型別的相關行為,但如果使用Extract Class (提煉類),你只需插入另一個元件就可以改變物件的行為。此外,子類只能用以表現一組變化。如果你希望一個類以幾種不同的方式變化,就必須使用委託。



7.  Extract Superclass 提煉超類

兩個類有相似特性。為這2個類建立一個超類,將相同特性移至超類


重複程式碼是系統中最糟糕的東西之一。如果你在不同地方做同一件事情,一旦需要修改那些動作,你就得平白做更多的修改。

重複程式碼的某種形式就是:2個類以相同的方式做類似的事情,或者以不同的方式做類似的事情。物件提供了一種簡化這種情況的機制,那就是繼承。但是,在建立這些具有共通性的類之前,你往往無法發現這樣的共通性,因此常常會在具有共通性的類出現之後,再開始建立其間的繼承結構。

另一種選擇就是 Extract Class(提煉類)。這2種方案之間的選擇其實就是繼承和委託之間的選擇。如果2個類可以共享行為,也可以共享介面,那麼繼承是比較簡單的做法。如果你選錯了,也總有 Replace Inheritance with Delegation (以委託取代繼承)這瓶後悔藥可吃。


8.  Extract Interface 提煉介面

若干客戶使用類介面中的同一子集,或者2個類的介面部分相同。將相同的子集提煉到一個獨立介面中。


類之間彼此互用的方式有若干種。“使用一個類”通常意味著用到該類的所有責任區。另一種情況是,某一組客戶只使用類責任區中的一個特定子集。再一種情況是,這個類需要與所有協助處理某些特定請求的類合作。

對於後2種情況,將真正用到的這部分責任分離出來通常很有意義,因為這樣可以使系統的用法更清晰,同時也更容易看清系統的責任劃分。如果新的類需要支援上述子集,也比較能夠看清子集內有些什麼東西。

在許多面向物件語言中,這種責任劃分是通過多繼承來實現的。在c#中可以運用介面來詔示並實現上述需求。

       Extract Subclass (提煉子類)和Extract Interface (提煉介面)之間有些相似之處。Extract Interface (提煉介面)只能提煉共通介面,不能提煉共通程式碼。使用Extract Interface (提煉介面)可能造成難聞的“重複”壞味道,幸而你可以運用Extract Class(提煉類)先把共通行為放進一個元件中,然後將工作委託給該元件,從而解決這個問題。如果有不少共通行為,Extract Superclass (提煉超類)會比較簡單,但是每個類只能有一個超類。

如果某個類在不同環境下扮演截然不同的角色,使用介面就是個好主意。你可以針對每個角色以Extract Interface (提煉介面)提煉出相應介面。另一種可以用Extract Interface (提煉介面)的情況是:你想要描述一個類的外部依賴介面。如果你打算將來加入其它種類的服務物件。只需要求它們實現這個介面即可。

9.   Collapse Hierarchy 摺疊繼承體系

超類和子類之間無太大區別。將它們和為一體。


如果你曾經編寫過繼承體系,就會知道,繼承體系很容易變得過分複雜。所謂重構繼承體系,往往是將函式和欄位在體系中上下移到。完成這些動作後,你很可能發現某個子類並未帶來該有的價值,因此需要把超類和子類合併起來。

10.  From TemPlate Method 塑造模板函式

你有一些子類,其中相應的某些函式以相同的順序執行類似的操作,但各個操作的細節不同。將這些操作分別放進獨立的函式中,並保持它們都有相同的簽名,於是原函式也就變得相同了,然後將原函式上移至超類。


繼承是避免重複行為的一個強大工具。無論何時,只要你看見2個子類之中有類似的函式,就可以把它們提升到超類。但是如果這些函式並不完全相同該這麼辦?仍有必要儘量避免重複,但又必須保持這些函式之間的實質差異。

常見的一種情況是:2個函式以相同的順序執行大致相近的操作,但是各操作不完全相同。這種情況下我們可以將執行的序列移至超類,並藉助多型保證各操作仍得以保持差異性。這樣的函式被稱為Template Method(模板函式)。

11.  Replace Inheritance with delegation 以委託取代繼承

某個子類只使用超類介面中的一部分,或是根本不需要繼承而來的資料。在子類中新建一個欄位用以儲存超類;調整子類函式,令它改而委託超類;然後去掉2者之間的繼承關係。


繼承是個好東西,但有時候它並不是你要的。你常常會遇到這樣的情況:一開始繼承了一個類,隨後發現超類中的許多操作並不真正適用於子類。這種情況下,你所擁有的介面並未真正反映出子類的功能。或者,你可能發現你從超類中繼承了一大堆子類並不需要的資料,抑或你可能發現超類中的某些protected函式對子類並沒有什麼意義。

你可以選擇容忍,並接受傳統說法:子類可以只使用超類功能的一部分。但這樣的結果是:程式碼傳達的資訊與你的意圖南轅北轍,你應該將它去除。

如果以委託取代繼承,你可以更清晰地表明:你只需要受委託的一部分功能。介面中的哪一部分應該被使用,哪一部分應該被忽略,完全由你主導控制。這樣做的成本則是需要額外寫出委託函式,但這些函式都給出簡單,極少可能出錯。

12.   Replace delegation with Inheritance 以繼承代替委託

你在2個類之間使用委託關係,並經常為整個介面編寫許多極簡單的委託函式。讓委託類繼承受託類。


       本項重構與Replace Inheritance with Delegation (以委託取代繼承)恰恰相反。如果你發現自己需要受託類中的所有函式,並且花費很大力氣編寫所有極簡單的委託函式,本重構可以幫助你輕鬆回頭使用繼承。

       2條告誡需牢記與心:首先,如果你並沒有使用受託類的所有函式,那麼就不應該使用Replace Delegation with Inheritance (以繼承取代委託),因為子類應該總是遵循超類的介面。如果過多的委託函式讓你煩心,你有別的選擇:你可以通過 Remove Middle Man (移除中間人)讓客戶端自己呼叫受託函式,也可以使用Extract Superclass (提煉超類)將2個類介面相同的部分提煉到超類中,然後讓2個類都繼承這個新的超類;你還可以用類似手法使用Extract Superclass (提煉超類)。

另一種需要當心的情況是:受託物件被不止一個其他物件共享,而且受託物件是可變的。在這種情況下,你就不能將委託關係替換為繼承關係,因為這樣就無法再共享資料了。資料共享是必須由受託物件承擔的一種責任,你無法把它轉給繼承關係。如果受託物件是不可變的,資料共享就不成問題,因為你大可放心地複製物件。