### Extract Method(提煉函數)
你有一段代碼可以被組織在一起并獨立出來。
將這段代碼放進一個獨立函數中,并讓函數名稱解釋該函數的用途。
~~~
void printOwing(double amount) {
printBanner();
//print details
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
~~~
=>
~~~
void printOwing(double amount) {
printBanner();
printDetails(amount);
}
void printDetails (double amount) {
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
~~~
**動機(Motivation)**
Extract Method是我最常用的重構手法之一。當我看見一個過長的函數或者一段需要注釋才能讓人理解用途的代碼,我就會將這段代碼放進一個獨立函數中。
有數個原因造成我喜歡簡短而有良好命名的函數。首先,如果每個函數的粒度都很小(finely grained),那么函數之間彼此復用的機會就更大;其次,這會使高層函數碼讀起來就像一系列注釋;再者,如果函數都是細粒度,那么函數的覆寫(overridden)也會更容易些。
的確,如果你習慣看大型函數,恐怕需要一段時間才能適應這種新風格。而且只有當你能給小型函數很好地命名時,它們才能真正起作用,所以你需要在函數名稱下點功夫。人們有時會問我,一個函數多長才算合適?在我看來,長度不是問題,關鍵在于函數名稱和函數本體之間的語義距離(semantic distance )。如果提煉動作 (extracting )可以強化代碼的清晰度,那就去做,就算函數名稱比提煉出來的代碼 還長也無所謂。
**作法(Mechanics)**
- 創造一個新函數,根據這個函數的意圖來給它命名(以它「做什么」來命名, 而不是以它「怎樣做」命名)。
- 即使你想要提煉(extract )的代碼非常簡單,例如只是一條消息或一個函數調用,只要新函數的名稱能夠以更好方式昭示代碼意圖,你也應該提煉它。但如果你想不出一個更有意義的名稱,就別動。
- 將提煉出的代^碼從源函數(source)拷貝到新建的目標函數(target)中。
- 仔細檢查提煉出的代碼,看看其中是否引用了「作用域(scope)限于源函數」的變量(包括局部變量和源函數參數)。
- 檢查是否有「僅用于被提煉碼」的臨時變量(temporary variables )。如果有,在目標函數中將它們聲明為臨時變量。
- 檢查被提煉碼,看看是否有任何局部變量(local-scope variables )的值被它改變。如果一個臨時變量值被修改了,看看是否可以將被提煉碼處理為一個查詢(query),并將結果賦值給相關變量。如果很難這樣做,或如果被修改的 變量不止一個,你就不能僅僅將這段代碼原封不動地離煉出來。你可能需要先使用 Split Temporary Variable,然后再嘗試提煉。也可以使用Replace Temp with Query 將臨時變量消滅掉(請看「范例」中的討論)。
- 將被提煉碼中需要讀取的局部變量,當作參數傳給目標函數。
- 處理完所有局部變量之后,進行編譯。
- 在源函數中,將被提煉碼替換為「對目標函數的調用」。
- 如果你將任何臨時變量移到目標函數中,請檢查它們原本的聲明式是否在被提煉碼的外圍。如果是,現在你可以刪除這些聲明式了。
- 編譯,測試。
**范例(examples):無局部變量(No Local Variables)**
在最簡單的情況下,Extract Method 易如反掌。請看下列函數:
~~~
void printOwing() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
// print banner
System.out.println ("**************************");
System.out.println ("***** Customer Owes ******");
System.out.println ("**************************");
// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
//print details
System.out.println ("name:" + _name);
System.out.println ("amount" + outstanding);
}
~~~
我們可以輕松提煉出「打印banner」的代碼。我只需要剪切、粘貼、再插入一個函數調用動作就行了:
~~~
void printOwing() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
//print details
System.out.println ("name:" + _name);
System.out.println ("amount" + outstanding);
}
void printBanner() {
// print banner
System.out.println ("**************************");
System.out.println ("***** Customer Owes ******");
System.out.println ("**************************");
}
~~~
**范例(Examples):有局部變量(Using Local Variables)**
果真這么簡單,這個重構手法的困難點在哪里?是的,就在局部變量,包括傳進源函數的參數和源函數所聲明的臨時變量。局部變量的作用域僅限于源函數,所以當我使用Extract Method 時,必須花費額外功夫去處理這些變量。某些時候它們甚至可能妨礙我,使我根本無法進行這項重構。
局部變量最簡單的情況是:被提煉碼只是讀取這些變量的值,并不修改它們。這種情況下我可以簡單地將它們當作參數傳給目標函數。所以如果我面對下列函數:
~~~
void printOwing() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
//print details
System.out.println ("name:" + _name);
System.out.println ("amount" + outstanding);
}
~~~
我就可以將「打印詳細信息」這一部分提煉為「帶一個參數的函數」:
~~~
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 printDetails (double outstanding) {
System.out.println ("name:" + _name);
System.out.println ("amount" + outstanding);
}
~~~
必要的話,你可以用這種手法處理多個局部變量。
如果局部變量是個對象,而被提煉碼調用了會對該對象造成修改的函數,也可以如法炮制。你同樣只需將這個對象作為參數傳遞給目標函數即可。只有在被提煉碼真的對一個局部變量賦值的情況下,你才必須采取其他措施。
**范例(Examples):對局部變量再賦值(Reassigning a Local Variable)**
如果被提煉碼對局部變量賦值,問題就變得復雜了。這里我們只討論臨時變量的問題。如果你發現源函數的參數被賦值,應該馬上使用Remove Assignments to Parameters。
被賦值的臨時變量也分兩種情況。較簡單的情況是:這個變量只在被提煉碼區段中使用。果真如此,你可以將這個臨時變量的聲明式移到被提煉碼中,然后一起提煉出去。另一種情況是:被提煉碼之外的代碼也使用了這個變量。這又分為兩種情況: 如果這個變量在被提煉碼之后未再被使用,你只需直接在目標函數中修改它就可以了;如果被提煉碼之后的代碼還使用了這個變量,你就需要讓目標函數返回該變量改變后的值。我以下列代碼說明這幾種不同情況:
~~~
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();
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 減少臨時變量。如果即使這么做了提煉依舊困難重重,我就會動用 Replace Method with Method Object,這個重構手法不在乎代碼中有多少臨時變量,也不在乎你如何使用它們。
- 譯序 by 侯捷
- 譯序 by 熊節
- 序言
- 前言
- 章節一 重構,第一個案例
- 起點
- 重構的第一步
- 分解并重組statement()
- 運用多態(Polymorphism)取代與價格相關的條件邏輯
- 結語
- 章節二 重構原則
- 何謂重構
- 為何重構
- 「重構」助你找到臭蟲(bugs)
- 何時重構
- 怎么對經理說?
- 重構的難題
- 重構與設計
- 重構與性能(Performance)
- 重構起源何處?
- 章節三 代碼的壞味道
- Duplicated Code(重復的代碼)
- Long Method(過長函數)
- Large Class(過大類)
- Long Parameter List(過長參數列)
- Divergent Change(發散式變化)
- Shotgun Surgery(散彈式修改)
- Feature Envy(依戀情結)
- Data Clumps(數據泥團)
- Primitive Obsession(基本型別偏執)
- Switch Statements(switch驚悚現身)
- Parallel Inheritance Hierarchies(平行繼承體系)
- Lazy Class(冗贅類)
- Speculative Generality(夸夸其談未來性)
- Temporary Field(令人迷惑的暫時值域)
- Message Chains(過度耦合的消息鏈)
- Middle Man(中間轉手人)
- Inappropriate Intimacy(狎昵關系)
- Alternative Classes with Different Interfaces(異曲同工的類)
- Incomplete Library Class(不完美的程序庫類)
- Data Class(純稚的數據類)
- Refused Bequest(被拒絕的遺贈)
- Comments(過多的注釋)
- 章節四 構筑測試體系
- 自我測試代碼的價值
- JUnit測試框架
- 添加更多測試
- 章節五 重構名錄
- 重構的記錄格式
- 尋找引用點
- 這些重構準則有多成熟
- 章節六 重新組織你的函數
- Extract Method(提煉函數)
- Inline Method(將函數內聯化)
- Inline Temp(將臨時變量內聯化)
- Replace Temp with Query(以查詢取代臨時變量)
- Introduce Explaining Variable(引入解釋性變量)
- Split Temporary Variable(剖解臨時變量)
- Remove Assignments to Parameters(移除對參數的賦值動作)
- Replace Method with Method Object(以函數對象取代函數)
- Substitute Algorithm(替換你的算法)
- 章節七 在對象之間搬移特性
- Move Method(搬移函數)
- Move Field(搬移值域)
- Extract Class(提煉類)
- Inline Class(將類內聯化)
- Hide Delegate(隱藏「委托關系」)
- Remove Middle Man(移除中間人)
- Introduce Foreign Method(引入外加函數)
- Introduce Local Extension(引入本地擴展)
- 章節八 重新組織數據
- Self Encapsulate Field(自封裝值域)
- Replace Data Value with Object(以對象取代數據值)
- Change Value to Reference(將實值對象改為引用對象)
- Replace Array with Object(以對象取代數組)
- Replace Array with Object(以對象取代數組)
- Duplicate Observed Data(復制「被監視數據」)
- Change Unidirectional Association to Bidirectional(將單向關聯改為雙向)
- Change Bidirectional Association to Unidirectional(將雙向關聯改為單向)
- Replace Magic Number with Symbolic Constant(以符號常量/字面常量取代魔法數)
- Encapsulate Field(封裝值域)
- Encapsulate Collection(封裝群集)
- Replace Record with Data Class(以數據類取代記錄)
- Replace Type Code with Class(以類取代型別碼)
- Replace Type Code with Subclasses(以子類取代型別碼)
- Replace Type Code with State/Strategy(以State/strategy 取代型別碼)
- Replace Subclass with Fields(以值域取代子類)
- 章節九 簡化條件表達式
- Decompose Conditional(分解條件式)
- Consolidate Conditional Expression(合并條件式)
- Consolidate Duplicate Conditional Fragments(合并重復的條件片段)
- Remove Control Flag(移除控制標記)
- Replace Nested Conditional with Guard Clauses(以衛語句取代嵌套條件式)
- Replace Conditional with Polymorphism(以多態取代條件式)
- Introduce Null Object(引入Null 對象)
- Introduce Assertion(引入斷言)
- 章節十一 處理概括關系
- Pull Up Field(值域上移)
- Pull Up Method(函數上移)
- Pull Up Constructor Body(構造函數本體上移)
- Push Down Method(函數下移)
- Push Down Field(值域下移)
- Extract Subclass(提煉子類)
- Extract Superclass(提煉超類)
- Extract Interface(提煉接口)
- Collapse Hierarchy(折疊繼承關系)
- Form Template Method(塑造模板函數)
- Replace Inheritance with Delegation(以委托取代繼承)
- Replace Delegation with Inheritance(以繼承取代委托)
- 章節十二 大型重構
- 這場游戲的本質
- Tease Apart Inheritance(梳理并分解繼承體系)
- Convert Procedural Design to Objects(將過程化設計轉化為對象設計)
- Separate Domain from Presentation(將領域和表述/顯示分離)
- Extract Hierarchy(提煉繼承體系)
- 章節十三 重構,復用與現實
- 現實的檢驗
- 為什么開發者不愿意重構他們的程序?
- 現實的檢驗(再論)
- 重構的資源和參考資料
- 從重構聯想到軟件復用和技術傳播
- 結語
- 參考文獻
- 章節十四 重構工具
- 使用工具進行重構
- 重構工具的技術標準(Technical Criteria )
- 重構工具的實用標準(Practical Criteria )
- 小結
- 章節十五 集成
- 參考書目