重構之--重新組織函數的幾種方法
重構手法中,很大一部分都是在對函數進行整理,很多問題也都來自Long Methods(過長的函數),下邊就介紹一下關於重新組織函數的幾種常用手法
1 Extract Method(提煉函數)
解釋:一個函數中有部分代碼可以被提取出來單獨抽成一個函數,並起一個能表達函數用途的函數名,這就是提煉函數(一個大函數可以提出很多小函數)
如:原函數
void PrintOwing(double amount) { PrintBanner(); //print details Console.WriteLine("name:"+_name); Console.WriteLine("amount:"+_amount); }
重構提煉後的函數
void PrintOwing(double amount) { PrintBanner(); //print details PrintDetails(); } private void PrintDetails() { Console.WriteLine("name:" + _name); Console.WriteLine("amount:" + _amount); }
動機:
Extract Method (提煉函數)是最常用的重構手法之一。當看見一個過長的函數或者一段需要註釋才能讓人理解用途的代碼,就應該將這段代碼放進一個獨立函數中。
簡短而命名良好的函數的好處:首先,如果每個函數的粒度都很小,那麽函數被復用的機會就更大;其次,這會使高層函數讀起來就想一系列註釋;再次,如果函數都是細粒度,那麽函數的覆寫也會更容易些。
一個函數多長才算合適?長度不是問題,關鍵在於函數名稱和函數本體之間的語義距離。如果提煉可以強化代碼的清晰度,那就去做,就算函數名稱必提煉出來的代碼還長也無所謂。
做法:
1、創造一個新函數,根據這個函數的意圖對它命名(以它“做什麽“命名,而不是以它“怎樣做”命名)。
即使你想要提煉的代碼非常簡單,例如只是一條消息或一個函數調用,只要新函數的名稱能夠以更好方式昭示代碼意圖,你也應該提煉它。但如果你想不出一個更有意義的名稱,就別動。
2、將提煉出的代碼從源函數復制到新建的明白函數中。
3、仔細檢查提煉出的代碼,看看其中是否引用了“作用域限於源函數”的變量(包括局部變量和源函數參數)。
4、檢查是否有“僅用於被提煉代碼段”的臨時變量。如果有,在目標函數中將它們聲明為臨時變量。
5、檢查被提煉代碼段,看看是否有任何局部變量的值被它改變。如果一個臨時變量值被修改了,看看是否可以將被提煉代碼處理為一個查詢,並將結果賦值給修改變量。如果很難這樣做,或如果被修改的變量不止一個,你就不能僅僅將這段代碼原封不動提煉出來。你可能需要先使用 Split Temporary Variable (分解臨時變量),然後再嘗試提煉。也可以使用 Replace Temp with Query (以查詢取代臨時變量)將臨時變量消滅掉。
6、將被提煉代碼段中需要讀取的局部變量,當做參數傳給目標函數。
7、處理完所有局部變量後,進行編譯。
8、在源函數中,將被提煉代碼段替換給對目標函數的調用。
如果你將如何臨時變量移到目標函數中,請檢查它們原本的聲明式是否在被提煉代碼段的外圍。如果是,現在可以刪除這些聲明式了。
9、編譯,測試。
註意:不要小看這個微小的變化,當一個函數很長的時候你就會發現他的威力,我們的項目肯定都比這個復雜,有時候你會發現有很多臨時變量在幹擾我們,沒關系,請接著往下看4、6、8都是為了解決這個問題
2 InLine Method(內聯函數)
解釋:一個函數的本體與名稱同樣清楚易懂。也就是說某個小函數的代碼體一看就知道什麽意思,已經沒有必要作為一個單獨的函數,可以在調用函數的地方直接用代碼體,然後移除該函數
如:原函數
int GetRating() { return MoreThanfiveLateDeliverise() ? 2 : 1; } bool MoreThanfiveLateDeliverise() { return _numberOfLateLiveries > 5; }
用內聯函數方法重構後
int GetRating() { return _numberOfLateLiveries > 5 ? 2 : 1; }
動機: 有時候你會遇到某些函數,其內部代碼和函數名稱同樣清晰易讀。也可能你重構了改函數,使得其內容和其名稱變得同樣清晰。果真如此,你應該去掉這個函數,直接使用其中的代碼。間接性可能帶來幫助,但非必要的間接性總是讓人不舒服。
另一種需要使用Inline Method (內聯函數)的情況是:你手上有一群不甚合理的函數。你可以將它們都內聯到一個大型函數中,再從中提煉出合理的小函數。實施Replace Method with Method Object (以函數對象取代函數)之前這麽做,往往可以獲得不錯的效果。你可以把所要的函數的所有調用對象的函數內容都內聯到函數對象中。比起既要移動一個函數,又要移動它所調用的其他所有函數,將整個大型函數作為整體來移動比較簡單。
如果別人使用了太多間接層,使得系統中所有函數都似乎只是對另一個函數的簡單委托,造成在這些委托動作之間暈頭轉向,那麽就使用 Inline Method (內聯函數)。當然,間接層有其價值,但不是所有間接層都有價值。試著使用內聯手法,可以找出那些有用的間接層,同時將那些無用的間接層去除。
做法:1、檢查函數,確定它不具多態性。如果子類繼承了這個函數,就不要將此函數內聯,因為子類午飯覆寫一個根本不存在的函數。
2、找出這個函數的所有被調用點。
3、將這個函數的所有被調用點都替換為函數本體。
4、編譯、測試。
5、刪除該函數定義。
註意:Inline Method (內聯函數)似乎很簡單。但情況往往並非如此。對於遞歸調用、多返回點、內聯至另一個對象中而該對象並無提供訪問函數……每種情況都可以寫上好幾頁如果遇到這些情況,那麽就不應該使用這個手法。
3 Inline Temp(內聯臨時變量)
解釋:你有一個臨時變量,只被一個簡單的表達式賦值一次,而它妨礙了其他重構手法。其實就是當你看到一個變量被賦值後只用一次且覺著多余,那麽就替換掉變量直接用表達式
如:原函數(看到這種代碼不覺著basePrice多余?)
double basePrice = anOrder.basePrice(); return (basePrice > 1000);
用Inline Temp重構後
return (anOrder.basePrice() > 1000);
動機:
你發現某個臨時變量被賦予某個函數的返回值,並且這個變量影響到了你用其他方法重構,則替換掉他!
做法:
1 檢查臨時變量賦值語句,確保等號右邊的表達式沒有副作用
2 如果這個臨時變量未聲明為final,則將其聲明為final,這樣可以確保該變量確實只被賦值了一次(因為final 變量賦值多次編譯報錯)
3 找到所有臨時變量引用點,將他們替換為賦值表達式
4 每次修改完後編譯測試
5 修改引用點後刪除變量聲明和賦值語句
6 編譯測試
註意:無
4 Replace Temp with Query(以查詢取代臨時變量)
解釋:你的程序以一個臨時變量保存某一個表達式的運算效果。將這個表達式提煉到一個獨立函數中。將這個臨時變量的所有引用點替換為對新函數的調用。此後,新函數就可以被其他函數調用。
如:原函數
double basePrice = _quantity*_itemPrice; if (basePrice > 1000) { return basePrice * 0.95; } else { return basePrice * 0.98; }
重構後
if (BasePrice() > 1000) { return BasePrice() * 0.95; } else { return BasePrice() * 0.98; } private int BasePrice() { return _quantity* _itemPrice; }
動機:臨時變量的問題在於:它們是暫時的,而且只能在所屬函數內使用。由於臨時變量只是在所屬函數內可見,所以它們會驅使你寫出更長的函數,因為只有這樣你才能訪問到需要的臨時變量。如果把臨時變量替換為一個查詢,那麽同一個類中的所有函數都可以獲得這份信息。這將帶給你極大幫助,使你能夠為這個類編寫更清晰地代碼。
Replace Temp with Query (以查詢取代臨時變量)往往是你運用Extract Method (提煉函數)之前必不可少的一個步驟。局部變量會使代碼難以被提煉,所以你應該盡可能把它們替換為查詢式。
這個重構手法較為簡單的情況是:臨時變量只被賦值一次,或者賦值給臨時變量的表達式不受其他條件影響。其他情況比較棘手,但也可能發生。你可能需要先運用Split Temporary Variable (分解臨時變量)或Separate Query form Modifier (將查詢函數和修改函數分離)使情況變得簡單一些,然後再替換臨時變量。如果你想替換的臨時變量是用來收集結果的)例如循環中的累加值),就需要將某些程序邏輯(例如循環)復制到查詢函數去。
做法:1、找出只被賦值一次的臨時變量。如果某個臨時變量被賦值超過一次,考慮使用Split Temporary Variable (分解臨時變量)將它們分解成多個變量。
2、將該變量聲明為const。
3、編譯。這可確保臨時變量的確只被賦值一次。
4、將“對該臨時變量賦值”之語句的等號右側部分提煉到一個獨立函數中。首先將函數聲明為private。日後你可能會發現有更多的類需要使用它。那是放松對它的保護也很容易。確保提煉出來的函數無任何副作用,也就是說該函數並不修改任何對象內容。如果它有副作用,就對它進行Separate Query form Modifier (將查詢函數和修改函數分離).
5、編譯,測試。
6、在該變量身上實施 Inline Temp (內聯臨時變量)。
我們常常使用臨時變量保存循環中的累加信息。在這種情況下,這個循環都可以被提煉為一個獨立函數,這也使原本的函數可以少掉幾行擾人的循環邏輯。有時候,你可能會在一個循環中累加好幾個值。這種情況下你應該針對每個累加值重復一遍循環,這樣就可以將所有臨時變量都替換為查詢。當然,循環應該很簡單,復制這些代碼才不會帶來危險。
註意: 運用此手法,你可能會擔心性能問題。和其他問題一樣,我們現在不管它,因為它十有八九根本不會造成任何影響。若是性能真的出了問題,你也可以在優化時期解決它。代碼組織良好,你往往能發現更有效的優化方案。如果沒用進行重構,好的優化方案就可能與你失之交臂。如果性能實在太糟糕,要把臨時變量放回去也很容易。(其實剛開始開發和重構時大可不必過多考慮性能問題,不然會寸步難行,代碼結構清楚了性能調優也會很方便,反正我是今天才明白)
5 Introduce Explaining Variable(引入解釋性變量)
6 Spilt Temporary Variable (分解臨時變量)
7 Remove AssignMents to Parameters(移除對參數的賦值)
8 Replace Method with Method Object(以函數對象取代函數)
9 Substitue Algorithm(替換算法)
解釋:想要把某個算法替換成另一個更加清晰的算法,將函數本體替換成另一個算法
原始函數(使用的if-else邏輯)
1 String findPerson(String[] person) 2 { 3 for (int i = 0; i < person.length(); ++i) 4 { 5 if(person[i].equals("Don")) 6 return "Don"; 7 else if (person[i].equals("John")) 8 return "John"; 9 else if (person[i].equals("Kent")) 10 return "Kent"; 11 } 12 return ""; 13 }
替換算法後的代碼(替換掉了多個if-else判斷,是不是簡潔許多)
String findPerson(String[] person) { StringList perList = Arrays.asList(new String[] {"Don" , "John" , "Kent"}); for (int i = 0; int i < person.length(); i++) { if (perList.contains(person[i])) return person[i]; } return ""; }
註:使用這種重構手法之前,盡可能的拆分原函數,只有先分解為了多個小函數,替換算法才容易下手
重構之--重新組織函數的幾種方法