重构-改善既有代码的设计:简化函数调用 (八)
1.??Rename Method 函數改名
函數的名稱未能揭示函數的用途。修改函數名稱。
大力提倡的一種編程風格是:將復雜的處理分解成小函數。但是,如果做得不好,這會使你費盡周折卻弄不清楚這些小函數各自的用途。要避免這種麻煩,關鍵就在于給函數起一個好名稱。函數的名稱應該準確表達它的用途。給函數命名有一個好辦法:首先考慮應該給這個函數寫上一句怎樣的注釋,然后想辦法將注釋變成函數名稱。
???????你常常無法第一次就給函數起一個好名稱。如果你看到一個函數名稱不能很好地表達它的用途,應該馬上加以修改。你的代碼首先是為人寫的,其次才是為計算機寫的。而人需要良好名稱的函數。如果給每個函數都起一個良好的名稱,也許你可以節約好多時間。起一個好名稱并不容易,需要經驗;要想成為一個真正的編程高手,起名的水平至關重要。當然,函數簽名中的其他部分也一樣重要。如果重新安排參數順序,能夠幫助提高代碼的清晰度,那就大膽地去做。還有 Add Parameter (添加參數)和Remove Parameter (移除參數)這2項武器。
2.?Add Parameter 添加參數
某個函數需要從調用端得到更多信息。為此函數添加一個對象參數,讓該對象帶進函數所需信息。
Add Parameter (添加參數)是一個很常用的重構手法。使用這項重構的動機很簡單:你必須修改一個函數,而修改后的函數需要一些過去沒有的信息,因此你需要給該函數添加一個參數。
???????需要說明的是:不使用本項重構的時機。除了添加參數外,你常常還有其他選擇。只要可能,其他選擇都比添加參數要好,因為它們不會增加參數列的長度。過長的參數列是不好的味道,因為程序員很難記住那么多參數而且長參數列往往伴隨著壞味道:數據泥團(Data Clumps)。
???????請看看現有的參數,然后問自己:你能從這些參數得到所需的信息嗎?如果回答是否定的,有可能通過某個函數提供所需信息嗎?你究竟把這些信息用于何處?這個函數是否應該屬于擁有該信息的那個對象所有?看看現有參數,考慮一下,加入新參數是否合適?也許你應該考慮使用 Introduce Parameter Object (引入參數對象)。
3.?Remove Parameter 移除參數
函數本體不再需要某個函數。將該參數去除。
程序員可能檢查添加參數,卻往往不愿意去掉它們。他們打的如意算盤是:無論如何,多余的參數不會引起任何問題,而且以后還可能用上它。
???????參數代表著函數所需的信息,不同的參數值有不同的意義。函數調用者必須為每一個參數操心該傳什么東西進去。如果你不去掉多余參數,就是讓你的每一位用戶多費一份心。是很不劃算的,更何況“去除參數”是非常簡單的一項重構。
???????但是,對于多態函數,情況有所不同。這種情況下,可能多態函數的另一份實現會使用這個參數,此時你就不能去除它。你可以添加一個獨立函數,在這些情況下使用。不過你應該先檢查調用者任何使用這個函數,以決定是否值得這么做。如果某些調用者已經知道他們正在處理的是一個特定的子類,并且已經做了額外工作找出自己需要的參數,或已利用對類體系的了解來避免取到null,那么就值得建立一個新函數,去除那多余的參數。如果調用者不需要了解函數所屬的類,你也可以繼續保持調用者無知而幸福的狀態。
4.Separate Query from Modifier 將查詢函數和修改函數分離
某個函數既返回對象狀態值,又修改對象狀態。建立2個不同的函數,其中一個負責查詢,另一個負責修改。
如果某個函數只是向你提供一個值,沒有任何看得到的副作用,那么這是個很有價值的東西。你可以任意調用這個函數,也可能把調用動作搬到函數的其他地方。明確表現出”有副作用”與“無副作用”2種函數之間的差異,是個很好的想法。任何有返回值的函數,都不應該有看得到的副作用。有些程序員甚至將此作為一條必須遵守的規則。
???????如果你遇到一個“既有返回值又有副作用”的函數,就應該試著將查詢動作從修改動作中分割出來。
???????有一種常見的優化辦法是:將查詢所得結果緩存于某個字段中,這么一來后續的重復查詢就可以大大加快速度。雖然這種做法改變了對象的狀態,但這一修改是覺察不到的,因為不論任何查詢,你總是獲得相同的結果。
5.Parameterize Method 令函數攜帶參數
若干函數做了類似的工作,但在函數本體中卻包含了不同的值。建立一個單一函數,以參數表達那些不同的值。
動機:你可能會發現這樣的2個函數:它們做著類似的工作,但因少數幾個值致使行為略為不同。這種情況下,你可以將這些各自分離的函數統一起來,并通過參數來處理那些變化,用以簡化問題。這樣的修改可以去除重復代碼,并提高靈活性,因為你可以用這個參數處理更多的變化情況。
6.Replace Parameter with Explicit Methods 以明確函數取代參數
你有一個函數,其中完全取決于參數值而采取不同香味。針對該參數的每個可能值,建立一個獨立函數。
Replace Parameter with Explicit Methods (以明確函數取代參數)恰恰相反于Parameterize Method (令函數攜帶參數)。如果某個參數有多種可能的值,而函數內又以條件表達式檢查這些參數值,并根據不同參數值做出不同的行為,那么就應該使用本項重構。調用者原本必須賦予參數適當的值,以決定該函數做出何種響應。現在,既然你提供了不同的函數給調用者使用,就可以避免出現條件表達式。此外你還可以獲得編譯期檢查的好處,而且接口也很清楚。如果以參數值決定函數行為,那么函數用戶不但需要觀察該函數,而且還要判斷參數值是否合法,而“合法的參數值”往往很少在文檔中被清楚地提出。
???????就算不考慮編譯期檢查的好處,只是為了獲得一個清晰地接口,也值得執行本項重構。哪怕只是給一個內部的布爾變量賦值,相較之下,switch。BeOn()也比Switch.SetState()要清楚的多。
???????但是,如果參數值不會對函數行為有太多影響,就不應該使用Replace Parameter with Explicit Methods (以明確函數取代參數)。如果情況真是這樣,而你也只需要通過參數為一個字段賦值,那么直接使用設值函數好了。如果的確需要條件判斷的行為,可考慮使用Replace Conditional with Polymorphism (以多態取代條件表達式)。
7.Preserve whole object 保持對象完整
你從某個對象中取出若干值,將它們作為某一次函數調用時的參數。改為傳遞整個對象。
有時候,你會將來自同一對象的若干項數據作為參數,傳遞給某個函數。這樣做的問題在于:萬一將來被調用函數需要新的數據項,你就必須查找并修改對此函數的所有調用。如果你把這些數據所屬的整個對象傳給函數,可以避免這種尷尬的處境,因為被調用函數可以向那個參數對象請求任何它想要的信息。
???????除了可以使參數列更穩固外,Preserve Whole Object (保持對象完整)往往還能提高代碼的可讀性。過長的參數列很難使用,因為調用者和被調用者都必須記住這些參數的用途。此外,不使用完整對象也會造成重復代碼,因為被調用函數無法利用完整對象中的函數來計算某些中間值。
???????不過事情總有2面:如果你傳的是數值,被調用函數就只依賴于這些數值,而不依賴它們所屬的對象。但如果你傳遞的是整個對象,被調用函數所在的對象就需要依賴參數對象。如果這會使你的依賴結構惡化,那么就不該使用Preserve Whole Object (保持對象完整)。
???????有的觀點認為:如果被調用函數只需要參數對象的其中一項數值,那么只傳遞那個數值會更好。這個觀點不能被認同:因為傳遞一項數值和傳遞一個對象,至少在代碼清晰度上是一致的。更重要的考量應該放在對象之間的依賴關系上。
???????如果被調用函數使用了來自另一個對象的很多數據項,這可能意味著該函數實際上應該被定義在那些數據所屬的對象中。所以,考慮使用Preserve Whole Object (保持對象完整)同時,你也該考慮Move Method(搬移函數)。
???????運用本項重構前,你可能還沒有定義一個完整對象,那么就應該先使用Introduce Parameter Object (引入參數對象)。
還有一種常見情況:調用者將自己的若干數值作為參數,傳遞給被調用函數。這種情況下,如果該對象有合適的取值函數,你可以使用this取代這些參數值,并且無需操心對象依賴問題。
8.Replace Parameter with Methods 以函數取代參數
對象調用某個函數,并將所得結果作為參數,傳遞給另一個函數。而接受該參數的函數本身也能夠調用前一個函數。讓參數接受者去除該項參數,并直接調用前一個函數。
如果函數可以通過其他途徑獲得參數值,那么它就不應該通過參數取得該值。過長的參數列會增加程序閱讀者的理解難度,因此應該盡可能縮短參數列的長度。
???????縮減參數列的辦法之一就是:看看參數接受端是否可以通過與調用端相同的計算來取得參數值。如果調用端通過其所屬對象內部的另一個函數來計算參數,并在計算過程中未曾引用調用端的其他參數,那么就應該可以將這個計算過程轉移到被調用端,從而去除該項參數。如果所調用的函數隸屬另一個對象,而該對象擁有調用端所屬對象的引用,前面所說的這些也同樣適用。
???????但是,如果參數值的計算過程依賴于調用端的某個參數,那么就無法去掉被調用端的參數,因為每次調用動作中,該參數值可能不同。另外,如果參數接受端并沒有參數發送端對象的引用,而你也不想加上這樣一個引用,那么也無法去除參數。
???????有時候,參數的存在是為了將來的靈活性。這種情況下仍然可以把這種多余參數拿掉。你應該只在必要關頭才添加參數,預先添加的參數很可能并不是你所需要的。對于這條規則,有個例外:如果修改接口會對整個程序造成非常痛苦的結果,那么可以考慮保留前人預先加入的參數。如果真是這樣,應該首先判斷修改接口究竟會造成多嚴重的后果,然后考慮是否應該降低給部位之間的依賴,以減少修改接口所造成的影響。穩定的接口確實很好,但是被凍結在一個不良接口上也是一個問題。
9.?Introduce Parameter Object 引入參數對象
某些參數總是很自然地同時出現。以一個對象取代這些參數。
你常常會看到特定的一組參數總是被一起傳遞。可能有好幾個函數都使用這一組參數,這些函數可能隸屬同一個類,也可能隸屬不同的類。這樣一組參數就是所謂的Data Clumps(數據泥團),我們可以運用一個對象包裝所有這些數據,再以該對象取代它們。哪怕只是為了把這些數據組織在一起,這樣做也是值得的。本項重構的價值在于縮短參數列,過長的參數列總是難以理解的。此外,新對象所定義的訪問函數還可以使代碼更具一致性,這又降低了理解和修改代碼的難度。
???????本項重構還可以帶給你更多好處。當你把這些參數組織到一起后,往往很快可以發現一些可被移至新建類的行為。通常,原本使用那些參數的函數對這一組參數會有一些共通的處理,如果將這些共通行為移到新對象中,你可以減少很多重復代碼。
10.Remove setting Method 移除設置函數
類中的某個字段應該在對象創建時被設值,然后就不再改變。去掉該字段的所有設值函數。
動機:如果你為某個字段提供了設值函數,這就暗示這個字段值可以被改變。如果你不希望在對象創建之后此字段還有機會被改變,那就不要為它提供設值函數。這樣你的意圖會更加清晰,并且可以排除其值被修改的可能性。
???????如果你保留了間接訪問變量的方法,就可能經常有程序員盲目使用它們。這些人甚至會在構造函數中使用設值函數。
11.Hide Method 隱藏函數
有一個函數,從來沒有被其他任何類用到。將這個函數修改為private。
重構往往促使你修改函數的可見度。提高函數可見度的情況很容易想象:另一個類需要用到某個函數,因此你必須提高該函數的可見度。但是要指出一個函數的可見度是否過高,就稍微困難一些。理想狀態下,你可以使用工具檢查所有函數,指出可被隱藏起來的函數。即使沒有這樣的工具,你也應該時常進行這樣的檢查。
???????一種特別常見的情況是:當你面對一個過于豐富、提供了過多行為的接口時,就值得將非必要的取值函數和設值函數隱藏起來。尤其當你面對的是一個簡單封裝的數據容器時,情況更是如此。隨著越來越多行為被放入這個類,你會發現許多設值/取值函數不再需要被公開,因此可以將它們隱藏起來。如果你把取值/設值函數設為private,然后在所有地方都直接訪問變量,那就可以放心移除取值/設值函數了。
12.Replace Constructor with Factory Method 以工廠函數取代構造函數
你希望在創建對象時不僅僅是做簡單的建構動作。將構造函數替換為工廠函數。
就是在派生子類的過程中以工廠函數取代類型碼。你可能常常需要根據類型碼創建相應的對象,現在,創建名單中還得加上子類,那些子類也是根據類型碼來創建。然而由于構造函數只能返回單一類型的對象,因此你需要將構造函數替換為工廠函數。
???????此外,如果構造函數的功能不能滿足你的需要,也可以使用工廠函數代替它。工廠函數也是Change Value to Reference (將值對象改為引用對象)的基礎。你也可以令你的工廠函數根據參數的個數和類型,選擇不同的構建行為。
做法:1、新建一個工廠函數,讓它調用現有的構造函數。
13.Encapsulate Downcast 封裝向下轉型
某個函數返回的對象,需要由函數調用者執行向下轉型(downcast)。將向下轉型動作移到函數中。
動機:向下轉型也許是無法避免的,但你仍然應該盡可能少做。如果你的某個函數返回一個值,并且你知道所返回的對象類型比函數簽名所昭告的更特化,你便是在函數用戶身上強加了非必要的工作。這種情況下你不應該要求用戶承擔向下轉型的責任,應該盡量為他們提供準確的類型。
???????以上所說的情況,常會在返回迭代器或集合的函數身上發生。此時你就應該觀察人們拿這個迭代器干什么用,然后針對性地提供專用函數。
14.Replace Error Code with Exception 以異常取代錯誤碼
某個函數返回一個特定的代碼,用以表示某種錯誤情況。改用異常。
程序中發現錯誤的地方,并不一定知道如何處理錯誤。當一段子程序發現錯誤時,它需要讓它的調用者知道這個錯誤,而調用者也可能將這個錯誤繼續沿著調用鏈傳遞上去。許多程序都使用特殊輸出來表示錯誤。
???????可以使用更好的錯誤處理方式:異常。它清楚地將“普通程序”和“錯誤處理”分開了,這使得程序更容易理解:代碼的可理解性應該是我們追求的目標。
15.Replace Exception with Test 以測試取代異常
面對一個調用者可以預先檢查的條件,你拋出一個異常。修改調用者,使它在調用函數之前先做檢查。
動機:異常的出現是程序語言的一大進步。但是,就像許多好東西一樣,異常會被濫用,從而變得不再讓人愉快。“異常”只應該被用于異常的、罕見的行為,也就是那些產生意料之外的錯誤的行為,而不應該成為條件檢查的替代品。如果你可以合理期望調用者在調用函數之前檢查某個條件,那么就應該提供一個測試,而調用者應該使用它。
總結
以上是生活随笔為你收集整理的重构-改善既有代码的设计:简化函数调用 (八)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 重构-改善既有代码的设计:简化条件表达式
- 下一篇: 重构-改善既有代码的设计:对象之间移动特