《重構:改善既有代碼設計》讀書筆記

目錄

第一章 重構,第一個案例

第二章 重構原則

第三章 代碼中的壞味道

第四章 構築測試體系

第五章 重構列表

第六章 重新組織函數

6.1 Extract Method(提煉函數)

6.2 Inline Method(內聯函數)

6.3 Inline Temp (內聯臨時變量)

6.4 Replace Temp with Query (以查詢取代臨時變量)

6.5 Introduce Explaining Variable (引入解釋性變量)

6.6 Split Temporary Variable (分解臨時變量)

6.7 Remove Assignments to Parameters (移除對參數的賦值)

6.8 Replace Method with Method Object (以函數對象取代函數)

6.9 Substitute Algorithm (替換算法)

第七章 在對象之間搬移特性

7.1 Move Method ( 搬移函數)

7.2 Move Field ( 搬移字段)

7.3 Extract Class (提煉類)

7.4 Inline Class (將類內聯化)

7.5 Hide Delegate (隱藏“委託關係”) 

7.6 Remove Middle Man (移除中間人)

7.7 Introduce Foreign Method (引入外加函數)

7.8 Introduce Local Extension (引入本地擴展)

第八章 重新組織數據

8.1 Self Encapsulate Field ( 自封裝字段)

8.2 Replace Data Value with Object (以對象取代數據值)

8.3 Change Value to Reference (將值對象改爲引用對象)

8.4 Change Reference to Value (將引用對象改爲值對象)

8.5 Replace Array with Object (以對象取代數組)

8.6 Duplicate Observed Data (複製“被監視數據”)

8.7 Change Unidirectional Association to Bidirectional(將單向關聯改爲雙向關聯)

8.8 Change Bidirectional Association to Unidirectional(將雙向關聯改爲單向關聯)

8.9 Replace Magic Number with Symbolic Constant(以字面常量取代魔法數)

8.10 Encapsulate Field ( 封裝字段)

8.11 Encapsulate Collection (封裝集合)

8.12Replace Record with Data Class (以數據類取代記錄)

8.13 Replace Type Code with Class ( 以類取代類型碼) 

8.14 Replace Type Code with Subclasses (以子類取代類型碼)

8.15 Replace Type Code with State/Strategy(以State/Strategy取代類型碼)

8.16 Replace Subclass with Fields ( 以字段取代子類)

第九章 簡化條件表達式

9.1 Decompose Conditional (分解條件表達式)

9.2 Consolidate Conditional Expression (合併條件表達式)

9.3 Consolidate Duplicate Conditional Fragments(合併重複的條件片段)

9.4 Remove Control Flag ( 移除控制標記)

9.5 Replace Nested Conditional with Guard Clauses(以衛語句取代嵌套條件表達式)

9.6 Replace Conditional with Polymorphism(以多態取代條件表達式)

9.7 Introduce Null Object (引入Null對象)

9.8 Introduce Assertion (引入斷言)

第十章 簡化函數調用

10.1 Rename Method (函數改名)

10.2 Add Parameter (添加參數)

10.3 Remove Parameter (移除參數)

10.4 Separate Query from Modifier(將查詢函數和修改函數分離)

10.5 Parameterize Method(令函數攜帶參數)

10.6 Replace Parameter with Explicit Methods(以明確函數取代參數)

10.7 Preserve whole object (保持對象完整)

10.8 Replace Parameter with Methods(以函數取代參數)    

10.9 Introduce Parameter Object(引入參數對象)

10.10 Remove setting Method (移除設置函數)

10.11 Hide Method (隱藏函數)

10.12 Replace Constructor with Factory Method(以工廠函數取代構造函數)

10.13 Encapsulate Downcast(封裝向下轉型)

10.14 Replace Error Code with Exception (以異常取代錯誤碼)

10.15 Replace Exception with Test (以測試取代異常)

第十一章 處理概括關係

11.1 Pull Up Field (字段上移)

11.2 Pull Up Method (函數上移)

11.3 Pull Up Constructor Body (構造函數本體上移) 

11.4 Push Down Method (函數下移)

11.5 Push Down Field (字段下移)

11.6 Extract Subclass (提煉子類)

11.7 Extract Superclass (提煉超類)

11.8 Extract Interface (提煉接口)

11.9 Collapse Hierarchy (摺疊繼承體系)

11.10 Form TemPlate Method (塑造模板函數)

11.11 Replace Inheritance with Delegation(以委託取代繼承)

11.12 Replace Delegation with Inheritance(以繼承取代委託)

第十二章 大型重構

12.1 Tease Apart Inheritance (梳理並分解繼承體系)

12.2 Convert Procedural Design to Objects(將過程化設計轉化爲對象設計)

12.3 Separate Domain from Presentation(將領域和表述/顯示分離)

12.4 Extract Hierarchy (提煉繼承體系)

第十三章 重構,複用與實現

第十四章 重構工具

第十五章 總結


最近一段時間,我們不門針對Java開發人員,組織學習了一下《重構:改善既有代碼設計》書籍,在學習的過程中,我也簡略的記了一些自己認爲的要點,給自己讀書留下點印記。

第一章 重構,第一個案例

第二章 重構原則

    重構(名詞):
        對軟件內部結構的一種調整,目的是在不改變軟件可觀察行爲的前提下,提高其可理解性,降低其修改成本。
    重構(動詞):
        使用一系列重構手法,在不改變軟件可觀察行爲的前提下,調整其結構。

    重構的目的:
        1.重構改進軟件設計
        2.重構是軟件更容易理解
        3.重構幫助找打bug
        4.重構提高編程速度
    重構時機:
        1.三次原則
        2.添加功能是重構
        3.修補錯誤時重構
        4.複審代碼時重構

    重構過程:
        它在一個目前可運行的程序上進行,在不改變程序行爲的前提習使其具備上述美好性質,使我們能夠繼續保持高速開發,從而增加程序的價值。

    間接層價值:
        1.允許邏輯共享
        2.分開解釋意圖和實現。
        3.隔離變化
        4.封裝條件邏輯

第三章 代碼中的壞味道

    1.重複代碼
    2.過長函數
    3.過大的類
    4.過長的參數列表
    5.發散式變化
    6.霰彈式修改
    7.依戀情結
    8.數據泥團
    9.基本類型偏執
    10.switch驚悚現身
    11.平行繼承體系
    12.冗贅類
    13.誇誇其談未來性
    14.令人迷惑的暫時字段
    15.過度耦合的信息鏈
    16.中間人
    17.狎暱關係
    18.異曲同工的類
    19.不完美的類庫
    20.純稚的數據類
    21.被拒絕的遺贈
    22.過多註釋

第四章 構築測試體系

撰寫測試代碼的最有用時機是在開始編程之前。頻繁地運行測試。每次編譯請把測試也考慮進去一每天 至少執行每個測試一次。

    編寫這些測試的目的是爲了提高程序員的生產率。

    功能測試就完全不同。它們用來保證軟件能夠正常運作。

    每當你收到bug報告,請先寫一個單元測試來暴露bug.

    哪怕只做一點點測試, 你也能從中受益。測試的要訣是:測試你最擔心出錯的部分,這樣你就能從測試工作中得到最大利益。

    編寫未臻完善的測試並實際運行,好過對完美測試的無盡等待。

    考慮可能出錯的邊界條件,把測試火力集中在那兒。

    當事情被認爲應該會出錯時,別忘了檢查是否拋出了預期的異常。

    任何測試都不能證明一個程序沒有bug。

    測試可以提高編程速度。

    不要因爲測試無法捕捉所有bug就不寫測試,因爲測試的確可以捕捉到大多數bug。

第五章 重構列表

每個重構手法都有如下五個部分:
    首先是名稱(name)。 建造一個重構詞彙表,名稱是很重要的。這個名稱也就是我將在本書其他地方使用的名稱。
    名稱之後是一個簡短概要(summary)。 簡單介紹此重構手法的適用情景,以及它所做的事情。這部分可以幫助你更快找到你所需要的重構手法。
    動機(motivation) 爲你介紹“爲什麼需要這個重構”和“什麼情況下不該使用這個重構”。
    做法(mechanics) 簡明扼要地一步一步介紹如何進行此一重構。
    範例(examples) 以一個十分簡單的例子說明此重構手法如何運作。
    重構的基本技巧一小步前進、頻繁測試。

第六章 重新組織函數

6.1 Extract Method(提煉函數)

    當看見一個過長的函數或者一段需要註釋才能讓人理解用途的代碼,就會將這段代碼放進一個獨立函數中。

    做法
        創造一個新函數,根據這個函數的意圖來對它命名(以它“做什麼”來命名,而不是以它“怎樣做”命名)。
            即使你想要提煉的代碼非常簡單,例如只是一條消息或一個函數調用,只要新函數的名稱能夠以更好方式昭示代碼意圖,你也應該提煉它。但如果 你想不出一個更有意義的名稱,就別動。
        將提煉出的代碼從源函數複製到新建的目標函數中。
        仔細檢查提煉出的代碼,看看其中是否引用了“作用域限於源函數”的變量(包括局部變量和源函數參數)。
        檢查是否有“僅用於被提煉代碼段"的臨時變量。如果有,在目標函數中將它們聲明爲臨時變量。
        檢查被提煉代碼段,看看是否有任何局部變量的值被它改變。如果-一個臨時 變量值被修改了,看看是否可以將被提煉代碼段處理爲-一個查詢,並將結果 賦值給相關變量。如果很難這樣做,或如果被修改的變量不止一個,你就不能僅僅將這段代碼原封不動地提煉出來。你可能需要先使用Split Temporary Variable (128),然後再嘗試提煉。也可以使用Replace Temp with Query (120) 將臨時變量消滅掉(請看“範例”中的討論)。
        將被提煉代碼段中需要讀取的局部變量,當作參數傳給目標函數。
        處理完所有局部變量之後,進行編譯。
        在源函數中,將被提煉代碼段替換爲對目標函數的調用。
            如果你將任何臨時變量移到目標函數中,請檢查它們原本的聲明式是否在被提煉代碼段的外圍。如果是,現在你可以刪除這些聲明式了.
        編譯,測試。

6.2 Inline Method(內聯函數)

    一個函數的本體與名稱同樣清楚易懂。
    如果別人使用了太多間接層,使得系統中的所有函數都似乎只是對另一個函數 的簡單委託,造成我在這些委託動作之間暈頭轉向,那麼我通常都會使用Inline Method(117)。

    做法
        檢查函數,確定它不具多態性。
                如果子類繼承了這個函數,就不要將此函數內聯,因爲子類無法覆寫一個根本不存在的函數。
        找出這個函數的所有被調用點。
        將這個函數的所有被調用點都替換爲函數本體.
        編譯,測試。
        刪除該函數的定義。

6.3 Inline Temp (內聯臨時變量)

    你有一個臨時變量,只被一個簡單表達式賦值一次, 而它妨礙了其他重構手法。

    做法
        檢查給臨時變量賦值的語句,確保等號右邊的表達式沒有副作用。
        如果這個臨時變量並未被聲明爲final,那就將它聲明爲final,然後編譯。
            這可以檢查該臨時變量是否真的只被賦值一 次。
        找到該臨時變量的所有引用點,將它們替換爲“爲臨時變量賦值"的表達式。
        每次修改後,編譯並測試。
        修改完所有引用點之後,刪除該臨時變量的聲明和賦值語句。
        編譯,測試。

6.4 Replace Temp with Query (以查詢取代臨時變量)

    你的程序以一個臨時變量保存某一表達式的運算結果。

    做法
        首先是簡單情況:
        找出只被賦值一-次的臨時變量。
            切如果某個臨時變量被賦值超過一次, 考慮使用Split Temporary Variable (128)將它分割成多個變量。
        將該臨時變量聲明爲final.
        編譯。
            功這可確保該臨時變量的確只被賦值一次。
        將“對該臨時變量賦值”之語句的等號右側部分提煉到一個獨立函數中。
            首先將函數聲明爲private。日後你可能會發現有更多類需要使用它,那時放鬆對它的保護也很容易。
            確保提煉出來的函數無任何副作用,也就是說該函數並不修改任何對象內容。如果它有副作用,就對它進行Separate Query from Modifler (279)。
        編譯,測試。
        在該臨時變量身上實施Inline Temp (119)。

6.5 Introduce Explaining Variable (引入解釋性變量)

    動機
        表達式有可能非常複雜而難以閱讀。這種情況下,臨時變量可以幫助你將表達式分解爲比較容易管理的形式。

    做法
        聲明一個final臨時變量,將待分解之複雜表達式中的一部分動作的運算結果賦值給它。
        將表達式中的“運算結果”這一部分,替換爲上述臨時變量。
            如果被替換的這一部分在代碼中重複出現,你可以每次一個,逐一替換。
        編譯,測試。
        重複上述過程,處理表達式的其他部分。

6.6 Split Temporary Variable (分解臨時變量)

    程序有某個臨時變量被賦值超過一次,它既不是循環變量,也不被用於收集計算結果。
    針對每次賦值,創造一個獨立、對應的臨時變量。

    做法
        在待分解臨時變量的聲明及其第一次被賦值處,修改其名稱。
            如果稍後之賦值語句是[i=i+某表達式]形式,就意味這是個結果收集變量,那麼就不要分解它。結果收集變量的作用通常是累加、字符串接合、寫入流或者向集合添加元素。
        將新的臨時變量聲明爲final.
        以該臨時變量的第二次賦值動作爲界,修改此前對該臨時變量的所有引用點,讓它們引用新的臨時變量。
        在第二次賦值處,重新聲明原先那個臨時變量。
        編譯,測試。
        逐次重複上述過程。每次都在聲明處對臨時變量改名,並修改下次賦值之前的引用點。

6.7 Remove Assignments to Parameters (移除對參數的賦值)

    代碼對一個參數進行賦值。 以一個臨時變量取代該參數的位置。

    做法
        建立一個臨時變量,把待處理的參數值賦予它。
        以“對參數的賦值"爲界,將其後所有對此參數的引用點,全部替換爲“對此臨時變量的引用”。
        修改賦值語句,使其改爲對新建之臨時變量賦值。
        編譯,測試。
            如果代碼的語義是按引用傳遞的,請在調用端檢查調用後是否還使用了這參數。也要檢查有多少個按引用傳遞的參數被賦值後又被使用。請儘量只以return方式返回一個值。如果需要返回的值不止一個,看看可否把需返回的大堆數據變成單一對象,或乾脆爲每個返回值設計對應的一個獨立函數

6.8 Replace Method with Method Object (以函數對象取代函數)

    你有一個大型函數,其中對局部變量的使用使你無法採用Extract Method (110)。
    將這個函數放進一個單獨對象中,如此- -來局部變量就成了對象內的字段。然後你
    可以在同一個對象中將這個大型函數分解爲多個小型函數。

    做法
        我厚着臉皮從Kent Beck [Beck]那裏偷來了下列做法。
        建立一個新類,根據待處理函數的用途,爲這個類命名。
        在新類中建立一個final字段,用以保存原先大型函數所在的對象。我們將這個字段稱爲“源對象”。同時,針對原函數的每個臨時變量和每個參數, 在新類中建立一個對應的字段保存之。
        在新類中建立一個構造函數,接收源對象及原函數的所有參數作爲參數。
        在新類中建立一個compute()函數。
        將原函數的代碼複製到compute()函數中。如果需要調用源對象的任何函數,請通過源對象字段調用。
        編譯。
        將舊函數的函數本體替換爲這樣一條語句:“創建上述新類的一個新對象,而後調用其中的compute()函數”。

6.9 Substitute Algorithm (替換算法)

    你想要把某個算法替換爲另一個更清晰的算法。
    將函數本體替換爲另一個算法。

    做法
        準備好另一個(替換用)算法,讓它通過編譯。
        針對現有測試,執行上述的新算法。如果結果與原本結果相同,重構結束。
        如果測試結果不同於原先,在測試和調試過程中,以舊算法爲比較參照標準。
            對於每個測試用例,分別以新舊兩種算法執行,並觀察兩者結果是否相同.這可以幫助你看到哪一個測試用例出現麻煩,以及出現了怎樣的麻煩.

第七章 在對象之間搬移特性

7.1 Move Method ( 搬移函數)

    你的程序中,有個函數與其所駐類之外的另一個類進行更多交流:調用後者,或被後者調用。
    在該函數最常引用的類中建立一個有着類似行爲的新函數。將舊函數變成一個單純的委託函數,或是將舊函數完全移除。

    做法
        檢查源類中被源函數所使用的一切特性(包括字段和函數) ,考慮它們是否 也該被搬移。
            如果某個特性只被你打算搬移的那個函數用到,就應該將它一併搬移。如果另有其他函數使用了這個特性,你可以考慮將使用該特性的所有函數全都一併搬移。有時候,搬移一組函數比逐一搬移簡單些。
        檢查源類的子類和超類,看看是否有該函數的其他聲明。
            如果出現其他聲明,你或許無法進行搬移,除非目標類也同樣表現出多態性。
        在目標類中聲明這個函數。
            由你可以爲此函數選擇一個新名稱——對目標類更有意義的名稱。
        將源函數的代碼複製到目標函數中。調整後者,使其能在新家中正常運行。
            如果目標函數使用了源類中的特性,你得決定如何從目標函數引用源對象。如果目標類中沒有相應的引用機制,就把源對象的引用當作參數,傳給新建立的日標函數。
            如果源函數包含異常處理,你得判斷邏輯上應誚由哪個類來處理這一異 常。如果應該由源類來負責,就把異常處理留在原地。
        編譯目標類。
        決定如何從源函數正確引用口標對象。.
            可能會有一個現成的字段或函數幫助你取得目標對象。如果沒有,就看能否輕鬆建立一個這樣的函數。如果還是不行,就得在源類中新建一個字段來保存目標對象。這可能是一個永久性修改,但你也可以讓它是暫時的,因爲後繼的其他重構項目可能會把這個新建字段去掉.
        修改源函數,使之成爲一個純委託函數。
        編譯,測試。
        決定是否刪除源函數,或將它當作一個委託函數保留下來。
            如果你經常要在源對象中引用目標函數,那麼將源函數作爲委託函數保留下來會比較簡單。.
        如果要移除源函數,請將源類中對源函數的所有調用,替換爲對目標函數的調用。
            你可以每修改一個引用點就編譯並測試一次。也可以通過一次“查找/替換”改掉所有引用點,這通常簡單一些。
        編譯,測試

7.2 Move Field ( 搬移字段)

    你的程序中,某個字段被其所駐類之外的另一個類更多地用到。 在目標類新建一個字段,修改源字段的所有用戶,令它們改用新字段。

    做法
        如果字段的訪問級是public使用Encapsulate Field (206)將它封裝起來。
            如果你有可能移動那些頻繁訪問該字段的函數,或如果有許多函數訪問某個字段,先使用Self Encapsulate Field(171)也許會有幫助。
        編譯,測試。
        在目標類中建立與源字段相同的字段,並同時建立相應的設值/取值函數。
        編譯目標類。
        決定如何在源對象中引用目標對象。
            首先看是否有一個現成的字段成函數可以助你得到目標對象,如果沒有,就看能否輕易建立這樣一個函數。如果還不行,就得在源類中新建一個字段來存放目標對象。這可能是個永久性修改,但你也可以讓它是暫時的,因爲後續重枸可能會把這個新建字段除掉.
        刪除源字段。
        將所有對源字段的引用替換爲對某個目標函數的調用。
            如果需要讀取該變量,就把對源字段的引用替換爲對目標取值函數的調用;如果要對該變量賦值,就把對源字段的引用替換成對設值函數的調用。如果源字段不是private的,就必須在源類的所有子類中查找源字段的引用點,並進行相應替換。
        口編譯,測試。

7.3 Extract Class (提煉類)

    某個類做了應該由兩個類做的事。
    建立一個新類,將相關的字段和函數從舊類搬移到新類。

    做法
        決定如何分解類所負的責任。
        建立一個新類,用以表現從舊類中分離出來的責任。
            如果舊類剩下的責任與舊類名稱不符,爲舊類更名。
        建立“從舊類訪問新類"的連接關係。
            有可能需要一個雙向連接。但是在真正需要它之前,不要建立“從新類通 往舊類"的連接。
        對於你想搬移的每-一個字段,運用Move Field (146)搬移之。
        每次搬移後,編譯、測試。
        使用Move Method(142)將必要函數搬移到新類。先搬移較低層函數(也就是“被其他函數調用”多於“調用其他函敷”者),再搬移較高層函數。 
        每次搬移之後,編譯、測試。
        檢查,精簡每個類的接口。
            如果你建立起雙向連接, 檢查是否可以將它改爲單向連接。
        決定是否公開新類。如果你的確需要公開它,就要決定讓它成爲引用對象還是不可變的值對象。

7.4 Inline Class (將類內聯化)

    某個類沒有做太多事情。
    將這個類的所有特性搬移到另一個類中,然後移除原類。

    做法
        在目標類身上聲明源類的public協議,並將其中所有函數委託至源類。
            如果“以一個獨立接口表示源類函數”更合適的話,就應該在內聯之前先使用Extract Interface (341).
        修改所有源類引用點,改而引用目標類。
            將源類聲明爲private,以斬斷包之外的所有引用可能。同時修改源類的名稱,這便可使編譯器幫助你捕捉到所有對於源類的隱藏引用點。
        編譯,測試。
        運用Move Method (142)和Move Field (146),將源類的特性全部搬移到目標類。
        爲源類舉行- 一個簡單的“喪禮”。

7.5 Hide Delegate (隱藏“委託關係”) 

    客戶通過一個委託類來調用另一個對象。
    在服務類上建立客戶所需的所有函數,用以隱藏委託關係。

    做法
        對於每一個委託關係中的函數,在服務對象端建立一個簡單的委託函數。
        調整客戶,令它只調用服務對象提供的函數。
            如果使用者和服務提供者不在同一個包,考慮修改委託函數的訪問權限,讓客戶得以在包之外調用它。
        每次調整後,編譯並測試。
        如果將來不再有任何客戶需要取用圖7-1的Delegate (受託類),便可移除服務對象中的相關訪問函數。
        編譯,測試。

7.6 Remove Middle Man (移除中間人)

    某個類做了過多的簡單委託動作。
    讓客戶直接調用受託類。

    做法
        建立一個函數,用以獲得受託對象。
        對於每個委託函數,在服務類中刪除該函數,並讓需要調用該函數的客戶轉調用受託對象。
        處理每個委託函數後,編譯、測試。

7.7 Introduce Foreign Method (引入外加函數)

    你需要爲提供服務的類增加一個函數,但你無法修改這個類。 
    在客戶類中建立一個函數,並以第一參數形式傳入一個服務類實例。

    做法
        在客戶類中建立一個函數,用來提供你需要的功能。
            這個函數不應該調用客戶類的任何特性。如果它需-一個值,把該值當作參數傳給它。
        以服務類實例作爲該函數的第一個參數。
        將該函數註釋爲:“ 外加函數(foreign method),應在服務類實現。”
            這麼一來,如果將來有機會將外加函數搬移到服務類中時,你便可以輕鬆找出這些外加函數。

7.8 Introduce Local Extension (引入本地擴展)

    你需要爲服務類提供一些額外函數, 但你無法修改這個類。
    建立一個新類,使它包含這些額外函數。讓這個擴展品成爲源類的子類或包裝類。

    做法
        建立一個擴展類,將它作爲原始類的子類或包裝類。
        在擴展類中加入轉型構造函數。
            所謂“轉型構造函數”是指“接受原對象作爲參數”的構造函數。如果採用子類化方案,那麼轉型構造函數應該調用適當的超類構造函數;如果採用包裝類方案,那麼轉型構造函數應該將它得到的傳入參數以實例變量的形式保存起來,用作接受委託的原對象。
        在擴展類中加入新特性。
        根據需要,將原對象替換爲擴展對象。
        將針對原始類定義的所有外加函數搬移到擴展類中。

第八章 重新組織數據

8.1 Self Encapsulate Field ( 自封裝字段)

    你直接訪問一個字段,但與字段之間的耦合關係逐漸變得笨拙。
    爲這個字段建立取值/設值函數,並且只以這些函數來訪問字段。

    做法
        爲待封裝字段建立取值/設值函數。
        找出該字段的所有引用點,將它們全部改爲調用取值/設值函數。
            如果引用點要讀取字段值,就將它替換爲調用取值函數;如果引用點要給字段賦值,就將它替換爲調用設值函數。
            你可以暫時將該字段改名,讓編譯器幫助你查找引用點。
        將該字段聲明爲private。
        複查,確保找出所有引用點。
        編譯,測試。

8.2 Replace Data Value with Object (以對象取代數據值)

    你有一個數據項,需要與其他數據和行爲一起使用纔有意義。
    將數據項變成對象。

    做法
        爲待替換數值新建一個類,在其中聲明一個final字段,其類型和源類中的待替換數值類型一樣。然後在新類中加入這個字段的取值函數,再加上一個接 受此字段爲參數的構造函數。
        編譯。
        將源類中的待替換數值字段的類型改爲前面新建的類。
        修改源類中該字段的取值函數,令它調用新類的取值函數。
        如果源類構造函數中用到這個待替換字段(多半是賦值動作),我們就修改構造函數,令它改用新類的構造函數來對字段進行賦值動作。
        修改源類中待替換字段的設值函數,令它爲新類創建一個實例。
        編譯,測試。
        現在,你有可能需要對新類使用Change Value to Reference (179)。

8.3 Change Value to Reference (將值對象改爲引用對象)

    你從一個類衍生出許多彼此相等的實例,希望將它們替換爲同一個對象。
    將這個值對象變成引用對象。

    做法
        使用Replace Constructor with Factory Method (304)。
        編譯,測試。
        決定由什麼對象負責提供訪問新對象的途徑。
            可能是一個靜態字典或一個註冊表對象.
            你也可以使用多個對象作爲新對象的訪問點。
        決定這些引用對象應該預先創建好,或是應該動態創建。
            如果這些引用對象是預先創建好的,而你必須從內存中將它們讀取出來,那麼就得確保它們在被需要的時候能夠被及時加載。
        修改工廠函數,令它返回引用對象。
            如果對象是預先創建好的,你就需要考慮:萬一有人索求一個其實並不存在的對象,要如何處理錯誤?
            你可能希望對工廠函數使用Rename Method (273),使其傳達這樣的信息:它返回的是一個既存對象。
        編譯,測試。


8.4 Change Reference to Value (將引用對象改爲值對象)

    你有一個引用對象,很小且不可變,而且不易管理。
    將它變成一個值對象。

    做法
        檢查重構目標是否爲不可變對象,或是否可修改爲不可變對象。
            如果該對象目前還不是不可變的,就使用Remove Setting Method (300),直到它成爲不可變的爲止。
            如果無法將該對象修改爲不可變的,就放棄使用本項重構。
        建立equals ()和hashCode()。
        編譯,測試。
        考慮是否可以刪除工廠函數,並將構造函數聲明爲public。

8.5 Replace Array with Object (以對象取代數組)

    你有一個數組,其中的元素各自代表不同的東西。
    以對象替換數組。對於數組中的每個元素,以一個字段來表示。

    做法
        新建一個類表示數組所擁有的信息,並在其中以一個public字段保存原先的數組。
        修改數組的所有用戶,讓它們改用新類的實例。
        編譯,測試。
        逐一爲數組元素添加取值/設值函數。根據元素的用途,爲這些訪間函數命名。修改客戶端代碼,讓它們通過訪問函數取用數組內的元素。每次修改後,編譯並測試。
        當所有對數組的直接訪問都轉而調用訪問函數後,將新類中保存該數組的字 段聲明爲private。
        編譯。
        對於數組內的每一個元素,在新類中創建一個類型相當的字段。修改該元素的訪問函數,令它改用上述的新建字段。
        每修改一個元素,編譯並測試。
        數組的所有元素都有了相應字段之後,刪除該數組。


8.6 Duplicate Observed Data (複製“被監視數據”)

    你有一些領域數據 置身於GUI控件中,而領域函數需要訪問這些數據。 將該數據複製到一個領域對象中。建立一個Observer模式,用以同步領域對象和GUI對象內的重複數據。

    做法
        修改展現類,使其成爲領域類的Observer[(GoF]。
            如果尚未有領城類,就建立一個。
            如果沒有“從展現類到領城類”的關聯,就將領域類保存於展現類的一個字段中。
        針對GUI類中的領域數據,使用Self Encapsulate Field (171)。
        編譯,測試。
        在事件處理函數中調用設值函數,直接更新GUI組件。
            在事件處理函數中放一個設值函數,利用它將GUI組件更新爲領城數據的當前值。當然這其實沒有必要,你只不過是拿它的值設定它自己。但是這樣使用設值函數,便是允許其中的任何動作得以於日後被執行起來,這是這一步驟的意義所在。
            進行這個改變時,對於組件,不要使用取值函數,應該直接取用,因爲稍後我們將修改取值函數,使其從領城對象(而非GUI組件)取值。設值函數也將做類似修改。
            確保測試代碼能夠觸發新添加的事件處理機制。
        編譯,測試。
        在領城類中定義數據及其相關訪問函數。
            確保領域類中的設值函數能夠觸發Observer模式的通報機制。 與對於被觀察的數據,在領域類中使用與展現類所用的相同類型(通常是字符串)來保存。後續重構中你可以自由改變這個數據類型。
        修改展現類中的訪問函數,將它們的操作對象改爲領域對象(而非GUI組件)。
        修改Obscrver的update(), 使其從相應的領域對象中將所需數據複製給GUI組件。
        編譯,測試。


8.7 Change Unidirectional Association to Bidirectional(將單向關聯改爲雙向關聯)

    兩個類都需要使用對方特性,但其間只有一條單向連接。
    添加一個反向指針,並使修改函數,能夠同時更新兩條連接。

    做法
        在被引用類中增加一個字段,用以保存反向指針。
        決定由哪個類———引用端還是被引用端———控制關聯關係。
        在被控端建立一個輔助函數,其命名應該清楚指出它的有限用途。
        如果既有的修改函數在控制端,讓它負責更新反向指針。
        如果既有的修改函數在被控端,就在控制端建立一個控制函數,並讓既有的修改函數調用這個新建的控制函數。

8.8 Change Bidirectional Association to Unidirectional(將雙向關聯改爲單向關聯)

    兩個類之間有雙向關聯,但其中一個類如今不再需要另一個類的特性。去除不必要的關聯。

    做法
        找出保存“你想去除的指針”的字段。檢查它的每一個用戶。判斷是否可以去除該指針。
            不但要檢查直接訪問點,也要檢查調用這些直接訪問點的函數。
            考慮有無可能不通過指針取得被引用對象.如果有可能,你就可以對取值函數使用Subtinule Algorihm(139),從而讓客戶在沒有相針的情況下也可以使用值取值函教。
            對於使用該字段的所有函數。考慮將被引周時象作爲參數傳進去。
        如果客戶使用了取值消數,先運Self Encapsulate Field  (171)將待刪除字段自我封裝起來,然後使用Sabtitute Algoritim (139)對付取值函數,它不再使用該字段。然後編譯,測試。
        如果客戶並未使用取值函數,那就直按修改待刪除字段的所有被引用點:改以其他途徑獲得該字段所保存的對象。每次修改後,編譯並測試。
        如果已經沒有任何函數使用待刪除字段,移除所有對該字段的更新邏輯,然後移除該字段。
            如果有許多地方時此宇段賦值,先達用Self Encapsulate Field (171)使這些地點改用同一個設值函教.編譯。測試。而後得這個設值函數的本體清空。再編譯,再測試。如果這些都可行,就可以將此字段和其設值函教,連同對設值函數數的所有調用,全部移除,
        編評,測試。

8.9 Replace Magic Number with Symbolic Constant(以字面常量取代魔法數)

    你有一個字面數值,帶有特別含義。
    創造一個常量,根據其意義爲它命名,並將上述的字面數值替換爲這個常量。

    做法
        聲明一個常量,令其值爲原本的魔法值。
        找出這個魔法值的所有引用點。
        檢查是否可以使用這個新聲明的常量來替換該魔法值。如果可以,便以此常量替換之。
        編譯。
        所有魔法值都被替換完畢後,編譯並測試。此時整個程序應該運轉如常,就像沒有做任何修改一樣。
            有個不錯的測試辦法: 檢查現在的程序是否可以被你輕鬆地修改常量值
            (這可能意味某些預期結果將有所改變,以配合這一新值。實際工作中並 非總是可以進行這樣的測試)。如果可行,這就是一個不錯的手法。


8.10 Encapsulate Field ( 封裝字段)

    你的類中存在一個public字段。將它聲明爲private,並提供相應的訪問函數。

    做法
        爲public字段提供取值/設值函數。
        找到這個類以外使用該字段的所有地點。如果客戶只是讀取該字段,就把引用替換爲對取值函數的調用:如果客戶修改了該字段值,就將此引用點替換爲對設值函數的調用。.
            如果這個字段是個對象,而客戶只不過是調用該對象的某個函數,那麼無論該函數是否改變對象狀態,都只能算是讀取該字段。只有當客戶爲該字.段賦值時,才能將其替換爲設值函數.
        每次修改之後,編譯並測試。
        將字段的所有用戶修改完畢後,把字段聲明爲private。
        編譯,測試。

8.11 Encapsulate Collection (封裝集合)

    有個函數返回一個集合。讓這個函數返回該集合的一個只讀副本,並在這個類中提供添加/移除集合元素的函數。

    做法
        加入爲集合添加/移除元素的函數。
        將保存集合的字段初始化爲一個空集合。
        編譯。
        找出集合設值函數的所有調用者。你可以修改那個設值函數,讓它使用上述 新建立的“添加/移除元素”函數:也可以直接修改調用端,改讓它們調用上述新建立的“添加/移除元素"函數.
            兩種情況下需要用到集合設值函數: (1)集合爲空時: (2)準備將原有集合替換爲另一個集合時。
            你或許會想運用Rename Method (273)爲集合設值函數改名:從setXxx() 改爲initializexxx()或replaceXxx()。
        編譯,測試。
        找出所有"通過取值函數獲得集合井作改其內容"的函數。逐一修改這些函數,讓它們改用添加/移除函數。每次修改後,編譯並測試。 
        修改完上述所有“通過取值函數獲得集合井修改集合內容"的函數後,修改 取值函數白身,使它返回該集合的個只讀副本。
            在Java 2中,你可以使用collect ion , unmodi fiablexx()得到該集合的只讀副本。
            在Java 1中,你應該返回集合的一份副本。
        編譯,測試。
        找出取值函散的所有用戶,從中找出應該存在於集合所屬對象內的代碼.運用Extract Method (110)和Move Method (142)將這些代碼移到宿主對象去。
             如果你使用Java 2, 那麼本項重構到此爲止。 如果你使用Java 1.1,那麼用戶也許會喜歡使用收舉,爲了提供這個枚舉,你應該值如下這樣做。
        修改現有取值函數的名字,然後添加一個新取值函數,使其返回個枚舉。找出舊取值函數的所有被使用點。將它們都改爲使用新取值函數。
        如果這一步跨度太大,你可以先使用Rename Method (273)修改原取值函數的名稱:再建立一個新取值函數用以返回枚舉:最後再修改所有調用者,使其調用新取值函數。
        編譯,測試。

8.12Replace Record with Data Class (以數據類取代記錄)

    你需要面對傳統編程環境中的記錄結構。
    爲該記錄創建一個“啞”數據對象。

    做法
        新建一個類,表示這個記錄。
        對於記錄中的每一項數據,在新建的類中建立對應的一個private字段,並提供相應的取值/設值函數。

8.13 Replace Type Code with Class ( 以類取代類型碼) 

    類之中有一個數值類型碼,但它並不影響類的行爲,以一個新的類替換該數值類型碼。

    做法
        爲類型碼建立一個類。
            這個類需要一個用以記錄類型碼的字段,其類型應該和類型碼相同,並應該有對應的取值函數。此外還應該用一組靜態變量保存允許被創建的實例,並以一個靜態函數根據原本的類型碼返回合適的實例。
        修改源類實現,讓它使用上述新建的類。
            維持原先以類型碼爲基礎的函數接口,但改變靜態字段,以新建的類產生代碼。然後,修改類型碼相關函數,讓它們也從新建的類中獲取類型碼。
        編譯,測試。
            此時,新建的類可以對類型碼進行運行期檢查。
        對於源類中每一個使用類型碼的函數,相應建立一個函數,讓新函數使用新建的類。
            你需要建立“以新類實例爲自變量”的函數,用以替換原先“直接以類型碼爲參數”的函數。你還需要建立一個“返回新類實例”的函數,用以替換原先“直接返回類型碼”的函數。建立新函數前,你可以使用Rename Method(273)修改原函數名稱,明確指出哪些函數仍然使用舊式的類型碼,這往往是個明智之舉。
        逐一修改源類用戶,讓它們使用新接口。
        每修改一個用戶,編譯並測試。
            你也可能需要一次性修改多個彼此相關的函數,才能保持這些函數之間的一致性,才能順利地編譯、測試。
        刪除使用類型碼的舊接口,並刪除保存舊類型碼的靜態變量。
        編譯,測試。


8.14 Replace Type Code with Subclasses (以子類取代類型碼)

    你有一個不可變的類型碼,它會影響類的行爲。以子類取代這個類型碼。

    做法
        使用Self Encapsulate Field (171)將類型碼自我封裝起來。
            如果類型碼被傳遞給構造函數,就需要將構造函數換成工廠函數。
        爲類型碼的每一個數值建立一個相應的子類。在每個子類中覆寫類型碼的取值函數,使其返回相應的類型碼值。
            這個值被硬編碼於return句 中(例如,return1).這看起來很骯髒,但只是權宜之計。當所有case子句都被替換後,問題就解決了。
        每建立一個新的子類,編譯並測試。
        從超類中刪掉保存類型碼的字段。將類型碼訪問函數聲明爲抽象函數。
        編譯,測試。

8.15 Replace Type Code with State/Strategy(以State/Strategy取代類型碼)

    你有一個類型碼,它會影響類的行爲,但你無法通過繼承手法消除它。以狀態對象取代類型碼。

    做法
        使用Self Encapsulate Field (171)將類型碼自我封裝起來。
        新建一個類,根據類型碼的用途爲它命名。這就是一個狀態對象。
        爲這個新類添加子類,每個子類對應一種類型碼。
            比起逐一添加,一次性加入所有必要的子類可能更簡單些。
        在超類中建立一個抽象的查詢函數,用以返回類型碼。在每個子類中覆寫該函數,返回確切的類型碼。
        編譯。
        在源類中建立一個字段,用以保存新建的狀態對象。
        調整源類中負責查詢類型碼的函數,將查詢動作轉發給狀態對象。
        調整源類中爲類型碼設值的函數,將一個恰 當的狀態對象子類賦值給“保存狀態對象”的那個字段。
        編譯,測試。

8.16 Replace Subclass with Fields ( 以字段取代子類)

    你的各個子類的唯一差別只在 “返回常量數據”的函數身上。修改這些函數,使它們返回超類中的某個(新增)字段,然後銷燬子類。

    做法
        對所有子類使用Replace Constructor with Factory Method (304)。
        如果有任何代碼直接引用子類,令它改而引用超類。
        針對每個常量函數,在超類中聲明一個final字段。
        爲超類聲明一個protected構造函數,用以初始化這些新增字段。
        新建或修改子類構造函數,使它調用超類的新增構造函數。
        編譯,測試。
        在超類中實現所有常最函數,令它們返回相應字段值,然後將該函數從子類中刪掉。
        每刪除一個常量函數,編譯並測試。
        子類中所有的常量函數都被刪除後,使用Inline Method(17)將子類構造函數內聯到超類的工廠函數中。
        編譯,測試。
        將子類刪掉。
        編譯,測試。
        重複“內聯構造函數、刪除子類"過程,直到所有子類都被刪除。

第九章 簡化條件表達式

9.1 Decompose Conditional (分解條件表達式)

    你有一個複雜的條件(if-then-else) 語句。從if、then、else三 個段落中分別提煉出獨立函數。

    做法
        將if段落提煉出來,構成一個獨立函數。
        將then段落和else段落都提煉出來,各自構成一個獨立函數。

9.2 Consolidate Conditional Expression (合併條件表達式)

    你有一系列條件測試,都得到相同結果。將這些測試合併爲一個條件表達式,並將這個條件表達式提煉成爲一個獨立函數。

    做法
        確定這些條件語句都沒有副作用。
            如果條件表達式有副作用,你就不能使用本項重構。
        使用適當的邏輯操作符,將一系列相關條件表達式合併爲一個。 
        編譯,測試。
        對合並後的條件表達式實施Extract Method (110)。

9.3 Consolidate Duplicate Conditional Fragments(合併重複的條件片段)

    在條件表達式的每個分支上有着相同的一段代碼。將這段重複代碼搬移到條件表達式之外。

    做法
        鑑別出“執行方式不隨條件變化而變化”的代碼。
        如果這些共通代碼位於條件表達式起始處,就將它移到條件表達式之前。
        如果這些共通代碼位於條件表達式尾端,就將它移到條件表達式之後。
        如果這些共通代碼位於條件表達式中段,就需要觀察共通代碼之前或之後的代碼是否改變了什麼東西。如果的確有所改變,應該首先將共通代碼向前或 向後移動,移至條件表達式的起始處或尾端,再以前面所說的辦法來處理。
        如果共通代碼不止一條語句, 應該首先使用Extract Method (110)將共通代碼提煉到一個獨立函數中,再以前面所說的辦法來處理。

9.4 Remove Control Flag ( 移除控制標記)

    在一系列布爾表達式中,某個變量帶有“控制標記" (control flag)的作用。以break語句或return語句取代控制標記。

    做法
    對控制標記的處理,最顯而易見的辦法就是使用Java提供的break語旬或 cont inue語句。
        找出讓你跳出這段邏輯的控制標記值。
        找出對標記變量賦值的語句,代以恰當的break語句或continue語句。
        每次替換後,編譯並測試。

    在未能提供break和cont inue語句的編程語言中,可以使用下述辦法。
        運用Extract Method (110),將整段邏輯提煉到一個獨立函數中。
        找出讓你跳出這段邏輯的控制標記值。
        找出對標記變量賦值的語句,代以恰當的return語句。
        每次替換後,編譯並測試。

9.5 Replace Nested Conditional with Guard Clauses(以衛語句取代嵌套條件表達式)

    函數中的條件邏輯使人難以看清正常的執行路徑。使用衛語句表現所有特殊情況。

    做法
        對於每個檢查,放進一個衛語句。
            衛語句要不就從函數中返回,要不就拋出一個異常。
        每次將條件檢查替換成衛語句後,編譯並測試。
            如果所有衛語句都導致相同結果,請使用Consolidate Conditional Expressions(240)。

9.6 Replace Conditional with Polymorphism(以多態取代條件表達式)

    你手上有個條件表達式,它根據對象類型的不同而選擇不同的行爲。
    將這個條件表達式的每個分支放進一個子類內的覆寫函數中,然後將原始函數聲明爲抽象函數。

    做法
        如果要處理的條件表達式是一個更大函數中的一部分,首先對條件表達式進行分析,然後使用Extract Method (10)將它提煉到一個獨立函數去。
        如果有必要,使用Move Method (142)將條件表達式放置到繼承結構的頂端。
        任選一個子類,在其中建立一個函數,使之覆寫超類中容納條件表達式的那個函數。將與該子類相關的條件表達式分支複製到新建函數中,並對它進行適當調整
            爲了順利進行這一步驟,你可能需要將超類中的某些private字段聲明爲protected.
        編譯,測試。
        在超類中刪掉條件表達式內被複制了的分支。
        編譯,測試。
        針對條件表達式的每個分支,覆上:述過程,直到所有分支都被移到子類內的函數爲止。
        將超類之中容納條件表達式的函數聲明爲抽象函數。

9.7 Introduce Null Object (引入Null對象)

       你需要再三檢查某對象是否爲null。將null值替換爲null對象。
     
     做法
        爲源類建立一個子類,使其行爲就像是源類的null版本。在源類和null子類中都加上isNu1l()函數,前者的isNull()應該返回false,後者的isNull()應該返回true。
            下面這個辦法也可能對你有所幫助:建立一個nullable接口,將isNu11()函數放在其中,讓源類實現這個接口.
            另外,你也可以創建一個測試接口,專門用來檢查對象是否爲null.
        編譯。
        找出所有“索求源對象卻獲得一個null” 的地方。修改這些地方,使它們改而獲得一個空對象。
        找出所有“將源對象與null做比較”的地方。修改這些地方,使它們調用isNull()函數。
            你可以每次只處理一個源對象及其客戶程序,編譯並測試後,再處理另一個源對象。
            你可以在“不該再出現nul1”的地方放上一些斷言,確保null的確不再出現。這可能對你有所幫助。
        編譯,測試。
        找出這樣的程序點:如果對象不是nu1l,做A動作,否則做B動作。
        對於每一個上述地點,在null類中覆寫A動作,使其行爲和B動作相同。
        使用上述被覆寫的動作,然後刪除“對象是否等於nul1”的條件測試。編譯:並測試。

9.8 Introduce Assertion (引入斷言)

    某一段代碼需要對程序狀態做出某種假設。以斷言明確表現這種假設。
    
    做法
        如果你發現代碼假設某個條件始終爲真,就加入一個斷言明確說明這種情況。
            你可以新建一個Assert類,用於處理各種情況下的斷言。

第十章 簡化函數調用

10.1 Rename Method (函數改名)

    函數的名稱未能揭示函數的用途。修改函數名稱。

    做法
        檢查函數簽名是否被超類或子類實現過。如果是,則需要針對每份實現分別進行下列步驟。
        聲明一個新函數,將它命名爲你想要的新名稱。將舊函數的代碼複製到新函數中,並進行適當調整。
        編譯。
        修改舊函數,令它將調用轉發給新函數。
            如果只有少數幾個地方引用舊函數,你可以大膽地跳過這一步驟。
        編譯,測試。
        找出舊函數的所有被引用點,修改它們,令它們改而引用新函數。每次修改後,編譯並測試。
        刪除舊函數。
            如果舊函數是該類public接口的一部分,你可能無法安全地刪除它。這種情況下,將它保留在原處,並將它標記爲deprecated (建議不使用)。
        編譯,測試。

10.2 Add Parameter (添加參數)

    某個函數需要從調用端得到更多信息。爲此函數添加一個對象參數,讓該對象帶進函數所需信息。

    做法
    Add Parameter (275)的做法和Rename Method (273)非常相似。
        檢查函數簽名是否被超類或子類實現過。如果是,則需要針對每份實現分別進行下列步驟。
        聲明一個新函數,名稱與原函數同,只是加上新添參數。將舊函數的代碼複製到新函數中。
            如果需要添加的參數不止-一個,將它們一次性添加進去比較容易。
        編譯。
        修改舊函數,令它調用新函數。
            如果只有少數幾個地方引用舊函數,你大可放心地跳過這一步驟.
            此時,你可以給參數提供任意值。但一般來說,我們會給對象參數提供null,給內置型參數提供一個明顯非正常值。對於數值型參數,我建議使用0以外的值,這樣你比較容易將來認出它.
        編譯,測試。
        找出舊函數的所有被引用點,將它們全部修改爲對新函數的引用。每次修改後,編譯並測試。
        刪除舊函數。
            如果舊函數是該類public接口的一部分,你可能無法安全地刪除它。這種情況下,請將它保留在原地,並將它標示爲deprecated (建議不使用)。
        編譯,測試。

10.3 Remove Parameter (移除參數)

    函數本體不再需要某個函數。將該參數去除。

    做法
    Remove Parameter (277)的做法和Rename Method (273)、Add Parameter (275)非常相似。.
        檢查函數簽名是否被超類或子類實現過。如果是,則需要針對每份實現分別進行下列步驟。
        聲明一個新函數,名稱與原函數同,只是去除不必要的參數。將舊函數的代碼複製到新函數中。
            如果需要去除的參數不止一個,將它們一次性去除比較容易。
        編譯。
        修改舊函數,令它調用新函數。
            如果只有少數幾個地方引用舊函數,你大可放心地跳過這一步驟。
        編譯,測試。
        找出舊函數的所有被引用點,將它們全部修改爲對新函數的引用。每次修改後,編譯並測試。
        刪除舊函數。
            如果舊函數是該類public接口的一部分,你可能無法安全地刪除它。這種情況下,將它保留在原處,並將它標記爲deprecated (建議不使用)。
        編譯,測試。

10.4 Separate Query from Modifier(將查詢函數和修改函數分離)

    某個函數既返回對象狀態值,又修改對象狀態。建立2個不同的函數,其中一個負責查詢,另一個負責修改。

    做法
        新建一個查詢函數,令它返回的值與原函數相同。
            觀察原函數,看它返回什麼東西。如果返回的是一個臨時變量,找出臨時變量的位置。
        修改原函數,令它調用查詢函數,並返回獲得的結果。
            原函數中的每個return句都應該像這樣: return newQuery(),而不應該返回其他東西。
        如果調用者將返回值賦給了一個臨時變量,你應該能夠去除這個臨時變量。
        編譯,測試。
        將調用原函數的代碼改爲調用查詢函數。然後,在調用查詢函數的那一行之前,加上對原函數的調用。每次修改後,編譯並測試。
        將原函數的返回值改爲void,並刪掉其中所有的return語句。

10.5 Parameterize Method(令函數攜帶參數)

    若干函數做了類似的工作,但在函數本體中卻包含了不同的值。建立一個單一函數,以參數表達那些不同的值。

    做法
        新建一個帶有參數的函數,使它可以替換先前所有的重複性函數。
        編譯。
        將調用舊函數的代碼改爲調用新函數。
        編譯,測試。
        對所有舊函數重複上述步驟,每次替換後,修改並測試。

10.6 Replace Parameter with Explicit Methods(以明確函數取代參數)

    你有一個函數,其中完全取決於參數值而採取不同香味。針對該參數的每個可能值,建立一個獨立函數。

    做法
        針對參數的每一種可能值,新建一個明確函數。
        修改條件表達式的每個分支,使其調用合適的新函數。
        修改每個分支後,編譯並測試。
        修改原函數的每一個被調用點,改而調用上述的某個合適的新函數
        編譯,測試。
        所有調用端都修改完畢後,刪除原函數。

10.7 Preserve whole object (保持對象完整)

    你從某個對象中取出若干值,將它們作爲某一次函數調用時的參數。改爲傳遞整個對象。

    做法
        對你的目標函數新添一個參數項,用以代表原數據所在的完整對象。
        編譯,測試。
        判斷哪些參數可被包含在新添的完整對象中。
        選擇上述參數之一,將被調用函數中原來引用該參數的地方,改爲調用新添參數對象的相應取值函數。
        刪除該項參數。
        編譯,測試。
        針對所有可從完整對象中獲得的參數,重複上述過程。
        刪除調用端中那些帶有被刪除參數的代碼。
            當然,如果調用端還在其他地方使用了這些參數,就不要刪除它們。
        編譯,測試。


10.8 Replace Parameter with Methods(以函數取代參數)    

    對象調用某個函數,並將所得結果作爲參數,傳遞給另一個函數。而接受該參數的函數本身也能夠調用前一個函數。讓參數接受者去除該項參數,並直接調用前一個函數。

    做法
        如果有必要,將參數的計算過程提煉到一個獨立函數中。
        將函數本體內引用該參數的地方改爲調用新建的函數。
        每次替換後,修改並測試。
        全部替換完成後,使用Remove Parameter (277)將該參數去掉。

10.9 Introduce Parameter Object(引入參數對象)

    某些參數總是很自然地同時出現。以一個對象取代這些參數。

    做法
        新建一個類,用以表現你想替換的一組參數。將這個類設爲不可變的。
        編譯。
        針對使用該組參數的所有函數,實施Add Parameter (275), 傳入上述新建類的實例對象,並將此參數值設爲null.
            如果你所修改的函數被其他很多函數調用,那麼可以保留修改前的舊函數,並令它調用修改後的新函數。你可以先對舊函數進行重構,然後逐一修改調用端使其調用新函數,最後再將舊函數刪除.
        對於Data Clumps中的每一項(在此均爲參數),從函數簽名中移除之,並修改調用端和函數本體,令它們都改而通過新的參數對象取得該值。
        每去除一個參數,編譯並測試。
        將原先的參數全部去除之後,觀察有無適當函數可以運用Move Method(142)搬移到參數對象之中。
            被搬移的可能是整個函數,也可能是函數中的一個段落。如果是後者,首先使用Extract Method(110)將該段落提煉爲一個獨立函數,再搬移這一新建函數。

10.10 Remove setting Method (移除設置函數)

    類中的某個字段應該在對象創建時被設值,然後就不再改變。去掉該字段的所有設值函數。

    做法
        檢查設值函數被使用的情況,看它是否只被構造函數調用,或者被構造函數所調用的另一個函數調用。
        修改構造函數,使其直接訪問設值函數所針對的那個變量。
            如果某個子類通過設值函數給超類的某個private字段設了值,那麼你就不能這樣修改。這種情況下你應該試着在超類中提供一個protected函數(最好是構造函數)來給這些字段設值。不論你怎麼做,都不要給超類中的函數起一個與設值函數混淆的名字。
        編譯,測試。
        移除這個設值函數,將它所針對的字段設爲final。
        編譯,測試。

10.11 Hide Method (隱藏函數)

    有一個函數,從來沒有被其他任何類用到。將這個函數修改爲private。

    做法
        經常檢查有沒有可能降低某個函數的可見度。
            使用lint類的工具,儘可能頻繁地檢查。當你在另一個類中移除對某個函數的調用時,也應該進行檢查。
            特別對設值函數進行上述的檢查。
        儘可能降低所有函數的可見度。
        每完成一組函數的隱藏之後,編譯並測試。
            如果有不適當的隱藏,編譯器很自然會檢驗出來,因此不必每次修改後都進行編譯。如有任何錯誤出現,很容易被發現。

10.12 Replace Constructor with Factory Method(以工廠函數取代構造函數)

    你希望在創建對象時不僅僅是做簡單的建構動作。將構造函數替換爲工廠函數。

    做法
        新建一個工廠函數,讓它調用現有的構造函數。
        將調用構造函數的代碼改爲調用工廠函數。
        每次替換後,編譯並測試。
        將構造函數聲明爲private。
        編譯。

10.13 Encapsulate Downcast(封裝向下轉型)

    某個函數返回的對象,需要由函數調用者執行向下轉型(downcast)。將向下轉型動作移到函數中。

    做法
        找出必須對函數調用結果進行向下轉型的地方。
            這種情況通常出現在返回一個集合或迭代器的函數中。
        將向下轉型動作搬移到該函數中。
        針對返回集合的函數,使用Encapsulate Collection (208)。

10.14 Replace Error Code with Exception (以異常取代錯誤碼)

    某個函數返回一個特定的代碼,用以表示某種錯誤情況。改用異常。

    做法
        決定應該拋出受控(checked)異常還是非受控(unchecked) 異常。
            如果調用者有責任在調用前檢查必要狀態,就拋出非受控異常。
            如果想拋出受控異常,你可以新建一個異常類,也可以使用現有的異常類.
        找到該函數的所有調用者,對它們進行相應調整,讓它們使用異常.
            如果函數拋出非受控異常,那麼就調整調用者,使其在調用函數前做適當檢查。每次修改後,編譯並測試。
            如果函數拋出受控異常,那麼就調整調用者,使其在try區段中調用該函數。
        修改該函數的簽名,令它反映出新用法。

    如果函數有許多調用者,上述修改過程可能跨度太大。你可以將它分成下列數個步驟. .
        決定應該拋出受控異常還是非受控異常。
        新建一個函數,使用異常來表示錯誤狀況,將舊函數的代碼複製到新函數中,並做適當調整。
        修改舊函數的函數本體,讓它調用上述新建函數。
        編譯,測試。
        逐一修改舊函數的調用者,令其調用新函數。每次修改後,編譯並測試。
        移除舊函數。

10.15 Replace Exception with Test (以測試取代異常)

    面對一個調用者可以預先檢查的條件,你拋出一個異常。修改調用者,使它在調用函數之前先做檢查。

    做法
        在函數調用點之前,放置一個測試語句,將函數內catch區段中的代碼複製到測試句的適當if分支中。
        在catch區段起始處加入一個斷言, 確保catch區段絕對不會被執行。
        編譯,測試。
        移除所有catch區段,然後將try區段內的代碼複製到try之外,然後移除try區段。
        編譯,測試。

第十一章 處理概括關係

11.1 Pull Up Field (字段上移)

    兩個子類擁有相同的字段。
    將該字段移至超類。

    做法
        針對待提升之字段,檢查它們的所有被使用點,確認它們以同樣的方式被使用。
        如果這些字段的名稱不同,先將它們改名,使每一個名稱都和你想爲超類字段取的名稱相同。
        編譯,測試。
        在超類中新建一個字段。
            如果這些字段是private的,你必須將超類的字段聲明爲protected,這樣子類才能引用它。
        移除子類中的字段。
        編譯,測試。
        考慮對超類的新建字段使用Self Encapsulate Field (171)。

11.2 Pull Up Method (函數上移)

    有些函數,在各個子類中產生完全相同的結果。將該函數移至超類。

    做法
        檢查待提升函數,確定它們是完全一致的。
            如果這些函數看上去做了相同的事,但並不完全一致,可使用Substitute Algorithm (139)讓它們變得完全一致。
        如果待提升函數的簽名不同,將那些簽名都修改爲你想要在超類中使用的簽名。
        在超類中新建一個函數,將某一個待提升函數的代碼複製到其中,做適當調整,然後編譯。
            如果你使用的是一種強類型語言,而待提升函數又調用了一個只出現於子類而未出現於超類的函數,你可以在超類中爲被調用函數聲明一個抽象函數。
            如果待提升函數使用了子類的一個字段,你可以使用Pull Up Field (320)將該字段也提升到超類;或者也可以先使用Self Encapsulate Field (171),然後在超類中把取值函數聲明爲抽象函數。
        移除一個待提升的子類函數。
        編譯,測試。
        逐一移除待提升的子類函數,直到只剩下超類中的函數爲止。每次移除之後都需要測試。
        觀察該函數的調用者,看看是否可以改爲使用超類類型的對象。

11.3 Pull Up Constructor Body (構造函數本體上移) 

    你在各個子類中擁有一些構造函數,它們的本體幾乎完全一致。在超類中新建一個構造函數,並在子類構造函數中調用它。

    做法
        在超類中定義一個構造函數。
        將子類構造函數中的共同代碼搬移到超類構造函數中。
            被搬移的可能是子類構造函數的全部內容。
            首先設法將共同代碼搬移到子類構造函數起始處,然後再複製到超類構造函數中。
        將子類構造函數中的共同代碼刪掉,改而調用新建的超類構造函數。
        如果子類構造函數中的所有代碼都是一樣的,那麼子類構造函數就只需要調用超類構造函數。
        編譯,測試。
            如果日後子類構造函數再出現共同代碼,你可以首先使用Extract Method(110)將那一部分提煉到一個獨立函數,然後使用Pull Up Method (322 )將該函數上移到超類。

11.4 Push Down Method (函數下移)

    超類中的某個函數只與部分(而非全部)子類有關。將這個函數移到相關的那些子類去。

    做法
        在所有子類中聲明該函數,將超類中的函數本體複製到每一個子類函數中。
            你可能需要將超類的某些字段聲明爲protected,讓子類函數也能夠訪問它們。如果日後你也想把這些字段下移到子類,通常就可以那麼做;否則應該使用超類提供的訪問函數。如果訪問函數並非public,你得將它聲明爲protected.
        刪除超類中的函數。
            你可能必須修改調用端的某些變量聲明或參數聲明,以便能夠使用子類。
            如果有必要通過一個超類對象訪問該函數,或你不想把該函數從任何子類中移除,再或超類是抽象類,那麼你就可以在超類中把該函數聲明爲抽象函數。
        編譯,測試。
        將該函數從所有不需要它的那些子類中刪掉。
        編譯,測試。

11.5 Push Down Field (字段下移)

    超類中的某個字段只被部分(而非全部)子類用到。將這個字段移到需要它的那些子類去。

    做法
        在所有子類中聲明該字段。
        將該字段從超類中移除。
        編譯,測試。
        將該字段從所有不需要它的那些子類中刪掉。
        編譯,測試。

11.6 Extract Subclass (提煉子類)

    類中的某些特性只被某些(而非全部)實例用到。
    新建一個子類,將上面所說的那一部分特性移到子類中。

    做法
        爲源類定義一個新的子類。
        爲這個新的子類提供構造函數。
            簡單的做法是:讓子類構造函數接受與超類構造函數相同的參數,並通過super調用超類構造函數。
            如果你希望對用戶隱藏子類的存在,可使用Replace Constructor withFactory Method (304).
        找出調用超類構造函數的所有地點。如果它們需要的是新建的子類,令它們改而調用新構造函數。
            如果子類構造函數需要的參數和超類構造函數的參數不同,可以使用Rename Method (273)修改其參數列.如果子類構造函數不需要超類構造函數的某些參數,可以使用Rename Method (273)將它們去除。
            如果不再需要直接創建超類的實例,就將超類聲明爲抽象類.
        逐一使用Push Down Method (328)和Push Down Field (329)將源類的特性移到子類去。
            和Extract Class (149)不同的是,先處理函數再處理數據,通常會簡單一些。
            當一個public函數被下移到子類後,你可能需要重新定義該函數的調用端的局部變量或參數類型,讓它們改而調用子類中的新函數。如果忘記進行這一步驟,編譯器會提醒你。
        找到所有這樣的字段:它們所傳達的信息如今可由繼承體系自身傳達(這一類字段通常是boolean變量或類型碼)。以Self Encapsulate Field (171)避免直接使用這些字段,然後將它們的取值函數替換爲多態常量函數。所有使用這些字段的地方都應該以 Replace Conditional with Polymorphism (255)重構。
            任何函數如果位於源類之外,而又使用了上述字段的訪問函數,考慮以Move Method (142)將它移到源類中,然後再使用Replace Conditional withPolymorphism (255).
        每次下移之後,編譯並測試。

11.7 Extract Superclass (提煉超類)

    兩個類有相似特性。爲這兩個類建立一個超類,將相同特性移至超類。

    做法
        爲原本的類新建一個空白的抽象超類。
        運用Pull Up Field (320)、Pull Up Method (322)和Pull Up Constructor Body(325)逐一將子類的共同元素上移到超類。
            先搬移字段,通常比較簡單。
            如果相應的子類函數有不同的簽名,但用途相同,可以先使用Rename Method (273)將它們的簽名改爲相同,然後再使用Pull Up Method (322)。
            如果相應的子類函數有相同的簽名,但函數本體不同,可以在超類中把它們的共同簽名聲明爲抽象函數。
            如果相應的子類函數有不同的函數本體,但用途相同,可試着使用Substitute Algorithm (139)把其中-一個函數的函數本體複製到另一個函數中。如果運轉正常,你就可以使用Pull Up Method (322)。
        每次上移後,編譯並測試。
        檢查留在子類中的函數,看它們是否還有共通成分。如果有,可以使用Extract Method(110)將共通部分再提煉出來,然後使用Pull Up Method (322)將提煉出的函數上移到超類。如果各個子類中某個函數的整體流程很相似,你也許可以使用Form Template Method (345)。
        將所有共同元素都上移到超類之後,檢查子類的所有用戶。如果它們只使用共同接口,你就可以把它們請求的對象類型改爲超類。

11.8 Extract Interface (提煉接口)

    若干客戶使用類接口中的同一子集,或者兩個類的接口有部分相同。將相同的子集提煉到一個獨立接口中。

    做法
        新建一個空接口。
        在接口中聲明待提煉類的共通操作。
        讓相關的類實現上述接口。
        調整客戶端的類型聲明,令其使用該接口。

11.9 Collapse Hierarchy (摺疊繼承體系)

    超類和子類之間無太大區別。將它們合爲一體。

    做法
        選擇你想移除的類:是超類還是子類?
        使用Pull up Field (320)和Pull up Method (322),或者Push Down Method (328)和Push Down Field (329),把想要移除的類的所有行爲和數據搬移到另一個類。
        每次移動後,編譯並測試。
        調整即將被移除的那個類的所有引用點,令它們改而引用合併後留下的類。這個動作將會影響變量的聲明、參數的類型以及構造函數。
        移除我們的目標;此時的它應該已經成爲一個空類。
        編譯,測試。


11.10 Form TemPlate Method (塑造模板函數)

    你有一些子類,其中相應的某些函數以相同順序執行類似的操作,但各個操作的細節上有所不同。
    將這些操作分別放進獨立函數中,並保持它們都有相同的簽名,於是原函數也就變得相同了。然後將原函數上移至超類。

    做法
        在各個子類中分解目標函數,使分解後的各個函數要不完全相同,要不完全不同。
        運用Pull Up Method (322)將各子類內完全相同的函數上移至超類。
        對於那些(剩餘的、存在於各子類內的)完全不同的函數,實施Rename Method(273),使所有這些函數的簽名完全相同。
            這將使得原函數變爲完全相同,因爲它們都執行同樣一組函數調用;但各子類會以不同方式響應這些調用。
        修改上述所有簽名後,編譯並測試。
        運用Pull Up Method (322)將所有原函數逐一上移至超類。在超類中將那些代表各種不同操作的函數定義爲抽象函數。
        編譯,測試。
        移除其他子類中的原函數,每刪除一個,編譯並測試。

11.11 Replace Inheritance with Delegation(以委託取代繼承)

    某個子類只使用超類接口中的一部分, 或是根本不需要繼承而來的數據。
    在子類中新建一個字段用以保存超類;調整子類函數,令它改而委託超類;然後去掉兩者之間的繼承關係。

    做法
        在子類中新建一個字段,使其引用超類的一個實例, 並將它初始化爲this.
        修改子類內的所有函數,讓它們不再使用超類,轉而使用上述那個受託字段。每次修改後,編譯並測試。
            你不能這樣修改子類中通過super調用超類函數的代碼,否則它們會陷入無限遞歸。這種函數只有在繼承關係被打破後才能修改。
        去除兩個類之間的繼承關係,新建一一個受託類的對象賦給受託字段。
        針對客戶端所用的每一個超類函數,爲它添加一個簡單的委託函數。
        編譯,測試。


11.12 Replace Delegation with Inheritance(以繼承取代委託)

    你在兩個類之間使用委託關係,並經常爲整個接口編寫許多極簡單的委託函數。讓委託類繼承受託類。

    做法
        讓委託端成爲受託端的一個子類。
        編譯。
            此時,某些函數可能會發生衝突:它們可能有相同的名稱,但在返回類型、異常指定或可見程度方面有所差異。你可以使用Remane Method (273)解決此類問題。
        將受託字段設爲該字段所處對象本身。
        去掉簡單的委託函數。
        編譯並測試。
        將所有其他涉及委託關係的代碼,改爲調用對象自身。
        移除受託字段。

第十二章 大型重構

12.1 Tease Apart Inheritance (梳理並分解繼承體系)

    某個繼承體系同時承擔兩項責任。建立兩個繼承體系,並通過委託關係讓其中一個可以調用另一個。

    做法
        首先識別出繼承體系所承擔的不同責任,然後建立一個二維表格(或者三維乃至四維表格,如果你的繼承體系夠混亂而你的繪圖工具夠酷的話),並以座標軸標示出不同的任務。我們將重複運用本重構,處理兩個或兩個以上的維度(當然,每次只處理一個維度)。
        判斷哪一項責任更重要些,並準備將它留在當前的繼承體系中。準備將另一項責任移到另一個繼承體系中。
        使用Exract Class (149)從當前的超類提煉出一個新類, 用以表示重要性稍低的責任,並在原超類中添加一個實例變量,用以保存新類的實例。
        對應於原繼承體系中的每個子類,創建上述新類的一個子類。在原繼承體系的子類中,將前一步驟所添加的實例變量初始化爲新建子類的實例。
        針對原繼承體系中的每個子類,使用Move Method (142)將其中的行爲搬移到與之對應的新建子類中。
        當原繼承體系中的某個子類不再有任何代碼時,就將它去除。
        重複以上步驟,直到原繼承體系中的所有子類都被處理過爲止。觀察新繼承體系,看看是否有可能對它實施其他重構手法,例如Pull Up Method (322)或Pull Up Field (320)。

12.2 Convert Procedural Design to Objects(將過程化設計轉化爲對象設計)

    你手上有一些傳統過程化風格的代碼。將數據記錄變成對象,將大塊的行爲分成小塊,並將行爲移入相關對象之中。

    做法
        針對每一個記錄類型,將其轉變爲只含訪問函數的啞數據對象。
            如果你的數據來自關係式數據庫,就把數據庫中的每個表變成一個啞數據對象。
        針對每一處過程化風格,將該處的代碼提煉到一個獨立類中。
            你可以把提煉所得的類做成一個Singleton(爲了方便重新初始化),或是把提煉所得的函數聲明爲static.
        針對每一段長長的程序,實施Extract Method (110)及其他相關重構將它分解。再以MoveMethod(142)將分解後的函數分別移到它所相關的啞數據類中。
        重複上述步驟,直到原始類中的所有函數都被移除。如果原始類是一個完全過程化的類,將它拿掉將大快人心。

12.3 Separate Domain from Presentation(將領域和表述/顯示分離)

    某些GUI類之中包含了領域邏輯。將領域邏輯分離出來,爲它們建立獨立的領域類。

    做法
        爲每個窗口建立一個領域類。
        如果窗口內有一張表格,新建一個類來表示其中的行,再以窗口所對應之領域類中的一一個集合來容納所有的行領域對象。
        檢查窗口中的數據。如果數據只被用於UI,就把它留着;如果數據被領域邏輯使用,而且不顯示於窗口上,我們就以Move Field (146)將它搬移到領域類中;如果數據同時被UI和領域邏輯使用,就對它實施Duplicate Observed Data(189),使它同時存在於兩處,並保持兩處之間的同步。
        檢查展現類中的邏輯。實施Extract Method (110)將展現邏輯從領域邏輯中分開。一旦隔離了領域邏輯,再運用Move Method (142)將它移到領域類。
        以上步驟完成後,你就擁有了兩組彼此分離的類:展現類用以處理GUI,領域類包含所有業務邏輯。此時的領域類組織可能還不夠嚴謹,更進一步的重構將解決這些問題。

12.4 Extract Hierarchy (提煉繼承體系)

    你有某個類做了太多工作,其中一部分工作是以大量條件表達式完成的。
    建立繼承體系,以一個子類表示一種特殊情況。

    做法
        我們爲你準備了兩組重構做法。第一種情況是:你無法確定哪些地方會發生變化。這時候你會希望每次一小步地前進。
            鑑別出一種變化情況。
                如果這種變化可能在對象生命週期的不同階段而有不同體現,就運用Extract Class (149)將它提煉爲一個獨立的類。
            針對這種變化情況,新建一個子類,並對原始類實施Replace Constructor with Factory Method (304)。再修改工廠函數,令它返回適當的子類實例。
            將含有條件邏輯的函數,一次一個,逐一複製到子類,然後在明確情況下(對子類明確,對超類不明確),簡化這些函數。
                如有必要隔離函數中的條件邏輯和非條件邏輯,可對超類實施ExtractMethod (110)。
            重複上述過程,將所有變化情況都分離出來,直到可以將超類聲明爲抽象類爲止。
            刪除超類中那些被所有子類覆寫的函數本體,並將它們聲明爲抽象函數。

        如果你非常清楚原始類會有哪些變化情況,可以使用另一種做法。
            針對原始類的每一種變化情況,建立一個子類。
            使用Replace Constructor with Factory Method (304)將原始類的構造函數轉變成工廠函數,並令它針對每一種變化情況返回適當的子類實例。
                如果原始類中的各種變化情況是以類型碼標示,先使用ReplaceTypeCode with Subelasses (223);如果那些變化情況在對象生命週期的不同階段會有不同體現,請使用Replace Tjype Code with State/Strategy (227).
            針對帶有條件邏輯的函數,實施Replace Conditional with Polymorphism (255)。
            如果並非整個函數的行爲有所變化,而只是函數一部分有所變化,請先運用Extract Method (110)將變化部分和不變部分隔開來。

第十三章 重構,複用與實現

第十四章 重構工具

第十五章 總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章