函數過長或者邏輯太混亂,重新組織和整理函數的代碼,使之更合理進行封裝。
1. Extract Method 提煉函數
提煉函數:(由複雜的函數提煉出獨立的函數或者說大函數分解成由小函數組成)你有一段代碼可以被組織在一起並獨立出來。將這段代碼放進一個獨立函數,並讓函數名稱解釋該函數的用途。
void printOwing() { //print bannerSystem.out.println(“*********”);System.out.println(“Banner”);System.out.println(“*********”);//print detailsSystem.out.println ("name: " + _name); System.out.println ("amount " + getOutstanding()); }
void printOwing() { printBanner(); printDetails(getOutstanding()); } Void printBanner() { //print banner System.out.println(“*********”); System.out.println(“Banner”); System.out.println(“*********”);}void printDetails (double outstanding) { System.out.println ("name: " + _name); System.out.println ("amount " + outstanding); } 過長的函數或者一段需要注釋才能讓人理解用途的代碼,就應該將這段代碼放進一個獨立函數中。簡短而命名良好的函數的好處:1)如果每個函數的粒度都很小,那麼函數被複用的機會就更大;2)這會使高層函數讀起來就想一系列注釋;3)如果函數都是細粒度,那麼函數的覆寫也會更容易些。
一個函數多長才算合適?長度不是問題,關鍵在於函數名稱和函數本體之間的語義距離。如果提煉可以強化代碼的清晰度,那就去做,就算函數名稱必提煉出來的代碼還長也無所謂。
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 (內嵌函式)。
當然,間接層有其價值,但不是所有間接層都有價值。試著使用內聯手法,可以找出那些有用的間接層,同時將那些無用的間接層去除。
3.Inline Temp 內聯臨時變數內聯臨時變數:(運算式代替臨時變數)你有一個臨時變數,只被一個簡單運算式賦值一次,而它妨礙了其他重構手法。將所有對該變數的引用動作,替換為對它賦值的那個運算式自身
double basePrice = anOrder.BasePrice();return basePrice(>1000);
return (anOrder.BasePrice() >1000);
Inline Temp(內聯臨時變數)多半是作為Replace Temp with Query(以查詢取代臨時變數)的一部分使用的,所以真正的動機出現在後者那裡。唯一單獨使用Inline Temp(內聯臨時變數)情況是:你發現某個臨時變數被賦予某個函數調用的傳回值。
一般來說,這樣的臨時變數不會有任何危害,可以放心把它留在那。但如果這個臨時變數妨礙了其他的重構手法,例如 Extract Method (提煉函數),就應該將它內聯化。
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; ……double basePrice() { return _quantity * _itemPrice; }
臨時變數的問題在於:它們是暫時的,而且只能在所屬函數內使用。由於臨時變數只是在所屬函數內可見,所以它們會驅使你寫出更長的函數,因為只有這樣你才能訪問到需要的臨時變數。如果把臨時變數替換為一個查詢,那麼同一個類中的所有函數都可以獲得這份資訊。這將帶給你極大協助,使你能夠為這個類編寫更清晰地代碼。
以查詢代替臨時變數往往是你運用Extract Method(提煉函數)之前必不可少的一個步驟。局部變數會使代碼難以被提煉,所以你應該儘可能把它們替換為查詢式。
這個重構手法較為簡單的情況是:臨時變數只被賦值一次,或者賦值給臨時變數的運算式不受其他條件影響。其他情況比較棘手,但也可能發生。你可能需要先運用Split Temporary Variable(分解臨時變數)或Separate Query form Modifier(將查詢函數和修改函數分離) 使情況變得簡單一些,然後再替換臨時變數。如果你想替換的臨時變數是用來收集結果的)例如迴圈中的累加值),就需要將某些程式邏輯(例如迴圈)複製到查詢函數去。
5.Introduce Explaining Variable 引入解釋性變數
引入解釋性變數:(複雜運算式分解為臨時解釋性變數)你有一個複雜的運算式。將該複雜運算式(或其中一部分)的結果放進一個臨時變數,以此變數名稱來解釋運算式用途。
if (Platform.ToUpperCass().indexOf("MAC") > -1 && (Browser.ToUpperCass().indexOf("Ie") > -1) && WasInitalized() ) {//do something }
const bool imMacOs = Platform.ToUpperCass().indexOf("MAC") > -1;const bool isIeBrowser = Browser.ToUpperCass().indexOf("Ie") > -1;const bool wasInitalized = WasInitalized();if (imMacOs && isIeBrowser && wasInitalized){//do something}
運算式有可能非常複雜而難以閱讀。這種情況下,臨時變數可以協助你將運算式分解為比較容易管理的形式。
在條件邏輯中,Introduce Explaining Variable(引入解釋性變數)是一個很常見的手法,但是最好盡量使用 Extract Method(提煉函數) 來解釋一段代碼的意義。畢竟臨時變數只在他所處的那個函數才有意義,局限性較大。函數則可以在對象的這個生命中都有用,並且可被其他對象使用。但有時候,當局部變數使Extract
Method(提煉函數)難以進行時,就可以使用Introduce Explaining Variable (引入解釋性變數).
6. Split Temporary Variable 分解臨時變數
分解臨時變數:(臨時變數不應該被賦值超過一次)你的程式有某個臨時變數被賦值超過一次,它既不是迴圈變數,也不被用於收集計算結果。針對每次賦值,創造一個獨立、對應的臨時變數
double temp = 2 + (_height + _width);Console.WriteLine(temp);temp = _height * _width;Console.WriteLine(temp);
const double perimeter = 2 + (_height + _width);Console.WriteLine(perimeter);const double area = _height * _width;Console.WriteLine(area);
臨時變數有各種不同用途,其中某些用途會很自然的導致臨時變數被多次賦值。“迴圈變數”和“結果收集變數”就是典型的例子:迴圈變數會隨迴圈的每次運行而改變;
結果收集變數負責將“通過這個函數的運算”而構成的某個值收集起來。
除了這2種情況,還有很多臨時變數儲存一段冗長代碼的運算結果,以便稍後使用。這種臨時變數應該只被賦值一次。如果它們被賦值超過一次,
就意味著它們在函數中承擔了一個以上的職責。如果臨時變數承擔多個責任,它就應該被替換為多個臨時變數,每個變數只承擔一個責任。同一個臨時變數承擔2件不同的事情,會令代碼閱讀者糊塗。
7.Remove Assigments to Parameters 移除對參數的賦值移除對參數的賦值:(不要對參數賦值)代碼對一個 參數賦值。以一個臨時變數取代該參數的位置。
int discount (int inputVal, int quantity, int yearToDate){ if (inputVal > 50) inputVal -= 2; }
int discount (int inputVal, int quantity, int yearToDate) { int result = inputVal; if (inputVal > 50) result -= 2; }
如果參數是Object,容易誤賦值。採用final來防止誤用參數:
要清楚“對參數賦值”這個說法的意思。如果你把一個名為foo的對象作為參數傳給某個函數,那麼“對參數賦值”意味著改變foo,使它引用另外一個對象。如果你在“被傳入對象”身上進行什麼操作,那沒什麼問題。這裡只針對“foo被改而指向另一個對象”這種情況來討論。
void aMethod(Object foo) { foo.modifyInSomeWay(); foo = anotherObject;}
這樣的做法降低了代碼的清晰度,而且混用了按值傳遞和按引用傳遞這2種參數傳遞方式。
在按值傳遞的情況下,對參數的任何修改,都不會對調用端造成任何影響。那些用過按引用傳遞方式的人可能會在這一點上犯糊塗。
另一個讓人糊塗的地方時函數本體內。如果你只以參數表示“被傳遞進來的東西”。那麼代碼會清晰地多,因為這種用法在所有語言都表現出相同語義。
8. Replace Method with Method object 函數對象取代函數
函數對象代替函數:(大函數變成類)你有一個大型函數,其中對局部變數的使用使你無法採用
Extract Method (提煉函數)。將這個大型函數放進一個單獨對象中,如此一來局部變數就成了對象內的欄位。然後你可以在同一個對象中將這個大型函數分解為多個小型函數。
class Order...double price() { double primaryBasePrice; double secondaryBasePrice; double tertiaryBasePrice; // long computation; ... }
或者可以採用static method
局部變數的存在會增加函數分解的難度。如果一個函數之中局部變數泛濫,那麼想分解這個函數是非常困難的。Replace Temp with Query (以查詢取代臨時變數)可以協助你減輕這一負擔,但有時候你會發現根本無法拆解一個需要拆解的函數。這種情況下,應該使用函數對象。
9.Substitute Algorithm 替換演算法
替換演算法:(函數本體替換為另一個演算法)你想要把某個演算法替換為另一個更清晰地演算法。將函數本體替換為另一個演算法。
String foundPerson(String[] people){ for (int i = 0; i < people.length; i++) { if (people[i].equals ("Don")){ return "Don"; } if (people[i].equals ("John")) { return "John"; } if (people[i].equals ("Kent")){ return "Kent"; } } return ""; }
String foundPerson(String[] people){ List candidates = Arrays.asList(new String[] {"Don", "John", "Kent"}); for (int i=0; i<people.length; i++) if (candidates.contains(people[i])) return people[i]; return ""; }
解決問題有好幾種方法。演算法也是如此。如果你發現做一件事可以有更清晰地方式,就應該以較清晰地方式取代複雜的方式。“重構”可以把一些複雜東西分解為較簡單的小塊,但有時你就必須刪除整個演算法,代之以簡單的演算法。隨著對問題有了更多理解,你往往會發現,在原先的做法之外,有更簡單的解決方案,此時就需要改變原來的演算法。如果你開始使用程式庫,而其中提供的某些功能/特性與你自己的代碼重複,那麼你也需要改變原先的演算法。
有時候你會想要修改原先的演算法,讓它去做一件與原先略有差異的事。這時候你也可以先把原先的演算法替換為一個較易修改的演算法,這樣後續的修改會輕鬆許多。使用這項重構之前,請先確定自己儘可能分解了原先函數。替換一個巨大而複雜的演算法是很困難的。只有先將它分解為較簡單的小型函數,然後你才能很有把握的進行演算法替換工作。