#
作法(Mechanics)
首先是簡單情況:
- 找出只被賦值一次的臨時變量==>如果某個臨時變量被賦值超過一次,考慮使用Split Temporay Variable(128)將它分割成多個變量.
- 將該臨時變量聲明為final.
- 編譯.==>這可確保該臨時變量的確只被賦值一次.
- 將[對臨時變量賦值]之語句的等號右側部分提煉到一個獨立函數中.
- ==>首先將函數聲明為private.日后你可能會發現有更多class需要使用它,彼時你可輕易放松對它的保護.
- ==>確保提煉出來的函數無任何連帶影響(副作用),也就是說該函數并不修改任何對象內容.如果它有連帶影響,就對它進行Separate Query from Modifier(279).
- 編譯,測試.
- 在該臨時變量身上實施Inline Temp(119).
我們常常使用臨時變量保存循環中的累加信息.在這種情況下,整個循環都可以被提煉為一個獨立函數,這也使原本的函數可以少掉幾行擾人的循環代碼.有時候,你可能會用單一循環累加好幾個值.這種情況下你應該針對每個累加值重復一遍循環,這樣就可以將所有臨時變量都替換為查詢式(query).當然,循環應該很簡單,復制這些代碼時才不會帶來危險.
運用此手法,呢可能會擔心性能問題.和其他性能問題一樣,我們現在不管它,因為它十有八九根本不會造成任何影響.如果性能真的出了問題,你也可以在優化時期解決它.如果代碼組織良好,那么你往往能夠發現更有效的優化法案;如果你沒有進行重構,好的優化法案就可能與你失之交臂.如果性能實在太糟糕,要把臨時變量放回去也是很容易的.
動機(Motivation) 臨時變量的問題在于:它們是暫時的,而且只能在所屬函數內使用.由于臨時變量只有在所屬函數內才可見,所以它們會驅使你寫出更長的函數,因為只有這樣你才能訪問到想要訪問的臨時變量.如果把臨時變量替換為一個查詢式(query method),那么同一個class中的所有函數都將可以獲得這份信息.這將帶給你極大幫助,使你能夠為這個class編寫更清晰的代碼.
Replace Temp with Query(120)往往是你運用Extract Method(110)之前必不可少的一個步驟.局部變量會使代碼難以被提煉,所以你應該盡可能把它們替換為查詢式.
這個重構手法較為直率的情況就是:臨時變量只被賦值一次,或者賦值給臨時變量的表達式不受其他條件影響.其他情況比較棘手,但也有可能發生.你可能需要先運用Split Temporary Variable(128)或Separate Query from Modifier(279)使情況變得簡單一些,然后再替換臨時變量.如果你想替換的臨時變量是用來收集結果的(例如循環中的累加值),你就需要將某些程序邏輯(例如循環)拷貝到查詢式(query method)去.
你的程序以一個臨時變量(temp)保存某一表達式的運算結果.
將這個表達式提煉到一個獨立函數中.將這個臨時變量的所有[被引用點]替換為[對新函數的調用].新函數可被其他函數使用.
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 base() * 0.98; ... double basePrice() { return _quantity * _itemPrice; }
-
作法(Mechanics)
-
如果這個臨時變量并未被聲明為final,那就將它聲明為final,然后編譯.==>這可以檢查該臨時變量是否真的只被賦值一次.
-
找到該臨時變量的所有引用點,將它們替換為[為臨時變量賦值]之語句中的等號右側表達式.
-
每次修改后,編譯并測試.
-
修改完所有引用電之后,刪除該臨時變量的聲明式和賦值語句.
-
編譯,測試.
動機(Motivation) Inline Temp(119)多半是作為Replace Temp with Query(120)的一部分來使用,所以真正的動機出現在后者那兒.唯一單獨使用Inline Temp(119)的情況是:你發現某個臨時變量被賦予某個函數調用的返回值.一般來說,這樣的臨時變量不會有任何危害,你可以放心地把它留在那兒.但如果這個臨時變量妨礙了其他的重構手法--例如Extract Method(110),你就應該就它inline化.
你有一個臨時變量,只被一個簡單表達式賦值一次,而它妨礙了其他重構方法.
將所有對該變量的引用動作,替換為對它賦值得那個表達式自身.
double basePrice = anOrder.basePrice(); return (basePrice > 1000); | | | | \ / return (anOrder.basePrice() > 1000);
-
作法(Mechanics)
-
檢查函數,確定它不具多態性(is not polymorphic).==>如果subclass繼承了這個函數,就不要將此函數inline化,因為subclass無法覆寫(override)一個根本不存在的函數.
-
找出這個函數的所有被調用點.
-
將這個函數的所有被調用點都替換為函數本體(代碼).
-
編譯,測試.
-
刪除該函數的定義.
被我這樣一寫,Inline Method(117)似乎很簡單.但情況往往并非如此.對于遞歸調用,多返回點,inline至另一個對象中而該對象并無提供訪問函數(accessors)......,每一種情況我都可以寫上好幾頁.我之所以不寫這些特殊情況,原因很簡單:如果你遇到了這樣的復雜情況,那么就不應該使用這個重構手法.
動機(Motivation) 有時候你會遇到某些函數,其內部代碼和函數名稱同樣清晰易讀.
另一種需要使用Inline Method(117)的情況是:你手上有一群組織不甚合理的函數.你可以將它們都inline到一個大型函數中,再從中提煉出組織合理的小型函數.Kent Beck發現,實施Replace Method with Method Object(135)之前先這么做,往往可以獲得不錯的效果.你可以把你所要的函數(有著你要的行為)的所有調用對象的函數內容都inline到method object(函數對象)中.比起既要移動一個函數,又要移動它所調用的其他所有函數,[將大型函數作為單一整體來移動]會比較簡單.
如果別人使用了太多間接層,使得系統中的所有函數都似乎只是對另一個函數的簡單委托(delegation),造成我在這些委托動作之間暈頭轉向,那么我通常都會使用Inline Method(117).當然,間接層有其價值,但不是所有間接層都有價值.試著使用inlining,我可以找出那些有用的間接層,同時將那些無用的間接層去除.
一個函數,其本體(method body)應該與其名稱(method name)同樣清楚易懂.
在函數調用點插入函數本體,然后移除該函數.
int getRating() { return (moreThanFiveLateDeliveries()) ? 2 : 1; } boolean moreThanFiveLateDeliveries() { return _numberOfLateDeliveries > 5; } | | | | \ / int getRating() { return (_numberOfLateDeliveries > 5) ? 2 : 1; }
范例(Examples):對局部變量再賦值(Reassigning) 如果被提煉碼對局部變量賦值,問題就變得復雜了.這里我們只討論臨時變量的問題.如果你發現源函數的參數被賦值,應該馬上使用Remove Assignments to Parameters(131).
被賦值的臨時變量也分兩種情況.較簡單的情況是:這個變量只在被提煉碼區段中使用.果真如此,你可以將這個臨時變量的聲明式移到被提煉碼中,然后一起提煉出去.另一種情況是:被提煉碼之外的代碼也使用了這個變量.這又分為兩種情況:如果這個變量在被提煉碼之后未再被使用,你只需直接在目標函數中修改它就可以了;如果被提煉碼之后的代碼還使用了這個變量,你就需要讓目標函數返回該變量改變后的指.我以下列代碼說明這幾種不同情況: void printOwing() { Enumeration e = _orders.elements(); double outstanding = 0.0;
printBanner();
// calculate outstanding while(e.hasMoreElements()) { Order each = (Order) e.nextElement(); outstanding += each.getAmount(); }
printDetails(outstanding); }
現在我把[計算]代碼提煉出來:
void printOwing() { printBanner(); double outstanding = getOutstanding(); printDetails(outstanding); }
double getOutstanding() { Enumeration e = _orders.elements(); double outstanding = 0.0; while(e.hasMoreElements()) { Order each = (Order) e.nextElement(); outstanding += each.getAmount(); } return outstanding; }
Enumeration變量e只在被提煉碼中用到,所以我可以將它整個搬到新函數中.double變量outstanding在被提煉碼內外都被用到,所以我必須讓提煉出來的新函數返回它.編譯測試完成后,我就把傳值改名,遵循我的一貫命名原則:
double getOutstanding() { Enumeration e = _orders.elements(); double result = 0.0; while(e.hasMoreElements()) { Order each = (Order) e.nextElement(); result += each.getAmount(); } return result; }
本例中的outstanding變量只是很單純地被初始化為一個明確初值,所以我可以只在新函數中對它初始化.如果代碼還對這個變量做了其他處理,我就必須將它的值作為參數傳給目標函數.對于這種變化,最初代碼可能是這樣:
void printOwing(double previousAmount) { Enumeration e = _orders.elements(); double outstanding = previousAmount * 1.2;
printBanner();
// calculate outstanding while(e.hasMoreElements()) { Order each = (Order) e.nextElement(); outstanding += each.getAmount(); }
printDetails(outstanding); }
提煉后的代碼可能是這樣:
void printOwing(double previousAmount) { double outstanding = previousAmount * 1.2;
printBanner(); double outstanding = getOutstanding(outstanding); printDetails(outstanding); } double getOutstanding(double initialValue) { double result = initialValue; Enumeration e = _orders.elements(); while(e.hasMoreElements()) { Order each = (Order) e.nextElement(); result += each.getAmount(); } return result; } 編譯并測試后,我再將變量outstanding的初始化過程整理一下: void printOwing(double previousAmount) { printBanner(); double outstanding = getOutstanding(previousAmount * 1.2); printDetails(outstanding); }
這時候,你可能會問:[如果需要返回的變量不止一個,又該怎么辦?]
你有數種選擇.最好的選擇通常是:挑選另一塊代碼來提煉.我比較喜歡讓每個函數都只返回一個值,所以我會安排多個函數,用以返回多個值.如果你使用的語言支持[輸出式參數](output parameters),你可以使用它們帶回多個回傳值.但我還是盡可能選擇單一返回值.
臨時變量往往為數眾多,甚至會使提煉工作舉步維艱.這種情況下,我會嘗試先運用Replace Temp with Query(120)減少臨時變量.如果即使這么做了提煉依舊困難重重,我就會動用Replace Method with Method Object(135),這個重構手法不在乎代碼中有多少臨時變量,也不在乎你如何使用它們.
|