重构-改善既有代码的设计:处理概括关系 (九)
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 (提煉超類)。
???????另一種需要當心的情況是:受托對象被不止一個其他對象共享,而且受托對象是可變的。在這種情況下,你就不能將委托關系替換為繼承關系,因為這樣就無法再共享數據了。數據共享是必須由受托對象承擔的一種責任,你無法把它轉給繼承關系。如果受托對象是不可變的,數據共享就不成問題,因為你大可放心地復制對象。
總結
以上是生活随笔為你收集整理的重构-改善既有代码的设计:处理概括关系 (九)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 重构全面总结
- 下一篇: 重构与模式:改善代码三部曲中的第三部