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

重構,第一個案例

1.1 起點

  • 如果發現現有的代碼結構使你無法很方便地添加新特性,那就先重構,使特性的添加比較容易進行後,再添加特性;

1.2 重構的第一步

  1. 爲即將修改的代碼建立可靠的測試環境 – 是人就會犯錯,所以需要可靠的測試;
  2. 測試結果能夠自我檢驗 – 成功”OK”,失敗列出失敗清單並打印行號 (自動化對比測試結果是提高效率的前提);

1.3 分解並重組”巨型”函數

  1. 切分提煉長函數(Extract Method),並移至更合適的類(Move Method) – 代碼塊越小,越容易管理;
  2. 重構技術 – 以微小的步伐修改程序,如果犯錯,很容易便可發現它;
  3. 變量重命名 – 代碼應表現自己的目的,而變量名是關鍵; 唯有寫出人類容易理解的代碼,纔是優秀的程序員;
  4. 去除臨時變量 – 臨時變量可能成爲問題,因爲它們只在所屬函數有效,從而助長冗長而複雜的函數;
  5. 重構的節奏 – 測試 → 小修改 → 測試 → 小修改 → …… , 正是這種節奏讓重構快速而安全地前進;

重構原則

2.1 何謂重構

  1. 調整代碼內部結構,在不改變功能的前提下,使其更易理解和修改;
  2. 兩頂帽子 – 添加新功能、重構:
    • 添加新功能時 – 不應修改既有代碼,只管添加並通過測試;
    • 重構時 – 只管改進程序結構,且只在絕對必要(接口變化)時才修改測試;
    • 開發過程中帽子經常變換 – 譬如增加功能時發現更改結構會更容易; 但無論何時,要清楚自己戴的是哪頂帽子;

2.2 爲何重構

  1. 改進軟件設計:維持原有設計使其便於閱讀理解避免腐敗變質; 消除重複代碼,方便未來修改;
  2. 使軟件更易理解:讓代碼更好地表達自己的用途 – a.方便自己以後查閱; b.協助自己理解不熟悉的代碼;
    • 早期重構 – “擦掉窗戶上的污垢,使你看得更遠”;
  3. 幫助找到Bug:越理解代碼越容易揪出Bug; 重構能更有效的寫出健壯的代碼;
  4. 提高編程速度:良好的設計是維持開發速度的根本;惡劣的設計會導致更多的調試、閱讀理解和尋找重複代碼;

2.3 何時重構

  1. 三次法則:第一次做某件事時只管去做;第二次做類似的事會反感但還可以做;第三次再做類似的事就應該重構;
  2. 添加功能時:a.若重構能使我更快地理解; b.若用其他方式來設計,添加功能會簡單方便很多;
  3. 修補錯誤時:如果收到一份錯誤報告 – 重構代碼,因爲它沒有清晰到你能一眼看出Bug;
  4. 複審代碼時:閱讀代碼 → 一定程度理解 → 提出建議 → 想到點子時考慮是否可通過重構輕鬆實現 → 重構 → 對代碼獲得更高層次的認識;
    • 複審者+原作者(結對編程複審) – 複審者提出建議 → 共同判斷是否能通過重構輕鬆實現 → 修改;
    • 大型設計的複審,用UML示意圖展現設計並以CRC卡展示軟件情節; 和團隊進行設計複審,和個人進行代碼複審;
  5. 如果發現昨天的決定已不適合今天的決定,放心改變這個決定以完成今天的工作,至於明天,回頭看今天覺得幼稚,那時還可以改變你的理解;
    • 希望程序(1)容易閱讀; (2)所有邏輯都只在唯一地點指定; (3)新的改動不會危及現有行爲; (4)儘可能簡單表達條件邏輯;

2.5 重構的難題

  1. 數據庫:在對象模型和數據庫模型之間插入一個分隔層,隔離兩個模型各自的變化;
    • 不需要一開始即插入分隔層,在發現對象模型變得不穩定時再產生它;
  2. 修改接口:接口只有被那些“找不到,即使找到也不能修改”的代碼使用時,纔會成爲重構的障礙;
    • 不要過早發佈接口 – 修改代碼所有權政策,使重構更順暢; (如果非要更改已發佈接口,讓舊接口調用新接口,並標記爲Deprecate);
  3. 難以通過重構手法完成的設計改動:考慮候選設計方案時,對比重構難度,有限挑選更易重構的設計,即使它不能覆蓋所有潛在需求;
  4. 何時不該重構:a.既有代碼太混亂或錯誤過多,應該“重寫”,重構前提是代碼大部分情況下運行正常;
    • b.項目已近最後期限時應避免重構;

2.6 重構與設計

  1. 預先設計(CRC卡等方式檢驗各種想法) → 擇一可接受方案 → 編碼 → 重構;
  2. 仍需思考潛在變化和靈活的解決方案,但不必逐一實現,而是問“簡單方案重構成該靈活方案有多大難度”,如果”容易”那實現目前的簡單方案即可;
  3. 哪怕你完全瞭解系統,也請實際度量它的性能,不要臆測,臆測會讓你學到一些東西,但十有八九你是錯的;

2.7 重構與性能

  1. 首先寫出可調的軟件,然後調整以獲得足夠的速度;
  2. 三種追求性能的編寫方法:
    • 時間預算法 – 分解設計時就做好預算,給每個組件分配一定資源–包括時間和執行軌跡;每個組件絕對不能超過自己的預算;
    • 持續關注法 – 任何時候做任何事時,都要設法保持系統的高性能; 作用不大,因爲過於分散,視角也和狹隘;
    • 關注性能熱點 – 90%的時間都花在了10%的代碼上; 使用度量工具監控程序的運行,讓它指出耗時/耗空間的熱點代碼,謹慎修改並多多測試以調優代碼;

代碼的壞味道

3.1 重複代碼(Duplicated Code)

  1. 如果在一個以上的地點看到相同的程序結構 – 設法將他們合而爲一;
    • 同一個類的兩個函數含有相同的表達式; (Extract Method)
    • 兩個互爲兄弟的子類內含相同表達式; (Extract Method → Pull Up Method(推入超類) → Form Template Method(提煉相似分割差異))

3.2 過長函數(Long Method)

  1. 擁有短函數的對象會活得比較好、比較長 – “間接層”能帶來的諸如解釋、共享、選擇能力都是由小型函數支持的;
  2. 一個好名字 – 讓小函數容易理解的真正關鍵,直觀瞭解而無需進入其中查看;
  3. 更積極地去分解函數:
    • 每當感覺需要註釋來說明某塊代碼時,就把其放入一個獨立函數中,並以其用途(而非實現手法)命名;
    • 條件表達式和循環也是提煉的信號 – Decompose Condition處理條件表達式;
  4. 消除大量的參數和臨時變量 – Replace Temp With Query消除臨時元素; Introduce Parameter Object / Preserve Whole Object將過長參數變得簡潔;

3.3 過大的類(Large Class)

  1. 太多成員變量 – 將類中相同前綴或字尾的成員變量提煉到某個組件內,如果該組件適合作爲一個子類,可用Extract Subclass;
  2. 太多代碼 – 有多個相同代碼則提取成函數; 或將部分代碼提取成類/子類來瘦身;
  3. GUI大類 – 把數據和行爲移到獨立的領域對象去,可能需要兩邊各保留一些重複數據並保持兩邊同步(Duplicate Observed Data);

3.4 過長參數列(Long Parameter List)

  • 太多參數會造成函數不易使用,且一旦需要更多數據就不得不修改它; 如果傳遞對象,只需(在函數內)增加一兩條請求就能得到更多的數據了;
    • Replace Parameter with Method – 若向已有對象(函數所屬類/另一參數)發出請求即可取代一個參數,則激活此重構手法;
    • Preserve Whole Object – 將來自同一對象的一堆數據(參數)收集起來,並以該對象替換它們;
    • Introduce Parameter Object – 若某些數據缺乏合理的對象歸屬,爲它們製造出一個”參數對象”;

3.5 發散式變化(Divergent Change)

  1. 一旦需要修改,只需跳到系統某處並只在此處修改 – 不行,則應考慮重構;
  2. 若某個類經常因爲不同的原因在不同的方向上發生變化 – 找出特定原因造成的變化,Extract Class將它們提煉到另一個類中;

3.6 霰彈式修改(Shotgun Surgery)

  • 若每遇到某種變化,都必須在許多不同的類內做出許多小修改 – 需使”外界變化”與”需要修改的類”趨於一一對應;
    • Move Method和Move Field把需要修改的代碼放進同一個類; 若無合適的類安置這些代碼,就創造一個(Inline Class);

3.7 依戀情節(Feature Envy)

  1. 函數對某個類的興趣高過對自己所處類的興趣 – 移到該去的地方
  2. 函數用到好幾個類的功能 – 哪個類擁有最多被此函數使用的數據,就把該函數置於哪(先分解後移動);

3.8 數據泥團(Data Clumps)

  • 那些總是綁在一起出現的數據,當刪掉其中某項,其他數據因而失去意義時 – 你應爲它們產生一個新對象;
    • 一旦提煉出新對象,即可着手尋找Feature Envy(3.7),分解,提煉,不必太久,所有的類都將充分發揮價值;

3.9 基本類型偏執(Primitive Obsession)

  • 多多運用小對象 – 譬如money類(幣值+幣種)、range類(起始值+結束值)等等;

3.10 switch驚悚現身(Switch/Case Statements)

  1. 多數情況下,看到switch/case語句,應考慮”多態”來替代;
  2. 若只是在單一函數中有些選擇事例,用Replace Parameter with Explicit Methods;

3.11 平行繼承體系(Parallel Inheritance Hierarchies)

  • 每當你爲某個類增加一個子類,也必須爲另一個類相應增加一個子類時 - 重構,讓一個繼承體系的實例引用另一個繼承體系的實例;

3.12 冗贅類(Lazy Class)

  • 如果某些子類沒有做足夠的工作,Collapse Hierarchy; 對於幾乎沒用的組件,Inline Class;

3.13 誇誇其談未來性(Speculative Generality)

  • 用不到,就去掉:a.沒有太大用的抽象類; b.不必要的委託; c.多餘的參數;

3.14 令人迷惑的暫時字段(Temporary Field)

  • 成員變量未被使用; 成員變量只在某個算法時有效(只是爲了減少傳參),將變量和算法提煉爲新的函數對象;

3.16 中間人(Middle Man)

  • 過度委託,譬如某個類接口有一半的函數都委託給了其他類(Middle Man) - 解決方案:
    • 直接和負責對象通信;
    • 若”不幹實事”的函數佔少數,Inline Method放進調用端;
    • 若這些Middle Man還有其他行爲,將其變爲實責對象的子類;

3.17 狎暱關係(Inappropriate Intimacy)

  • 過分狎暱的類必須拆散 – a.移動方法或成員變量; b.雙向關聯改爲單向; c.提煉共同點到新類; d.委託其他類傳遞相思; e.委託取代繼承;

3.18 異曲同工的類(Alternative Classes with Different Interfaces)

  • 若兩個函數做同一件事,卻有着不同的簽名 – 移動,歸併,或提煉到超類中;

3.20 純粹的數據類(Data Class)

  • 除了字段及讀寫函數,無一長物 – a.封裝public字段; b.恰當封裝容器類字段; c.移除不應修改的字段的設置函數; d.提煉調用函數以隱藏取值/設值函數;

3.21 被拒絕的遺贈(Refused Bequest)

  • 子類只運用了父類的一部分函數和數據 – 爲子類建立一個兄弟類,將所有用不到的字段/函數下移至兄弟類,保證超類的純粹;

3.22 過多的註釋(Comments)

  • 註釋之所以存在是因爲代碼很糟糕 – 當需要撰寫註釋時先嚐試重構,試着讓註釋都變得多餘;
    • 若需要註釋解釋一塊代碼做了什麼 – 提煉函數(Extract Method);
    • 若函數已提煉,仍需解釋其行爲 – 函數更名(Rename Method);
    • 如果需要註釋說明某些系統的需求規格 – 引入斷言(Introduce Assertion);

構築測試體系

  • 重構的前提 – 擁有可靠的測試環境; 並且,編寫優良的測試程序,可極大提高編程速度.

4.1 自測試代碼的價值

  • 編程時間消耗:a.編寫代碼(20%); b.決定下一步幹什麼(10%); c.設計(20%); d.調試(50%) – 修復很快,找出錯誤則像是噩夢一場;
  • 確保所有測試都完全自動化,讓它們檢查自己的測試結果;
  • 一套測試就是一個強大的bug偵測器,能大大縮減查找bug所需要的時間;
  • 頻繁地測試 – 極限編程方法之一:
    • 寫好一點功能,就立即添加測試,分量越小,越能輕鬆找到錯誤的源頭;
    • 每次編譯請把測試也考慮進去 – 每天至少執行每個測試一次;
    • 重構時,只需運行當下正在開發或整理的這部分代碼測試;
  • 編寫測試代碼時,一開始先讓它們失敗,確保測試機制可運行;
  • 每當收到功能測試的bug報告,請先寫一個單元測試來暴露這個bug;

4.3 添加更多測試

  • 觀察類該做的所有事情,然後針對任何一項功能的任何一種可能失敗情況,進行測試;
    • 風險驅動 – 測試的目的是找出可能出現的錯誤,因此沒必要去測試那些Public下的簡單讀寫字段;
    • 避免完美 – 將時間耗費在爲擔心出錯的部分寫測試;
    • 集中火力 – 考慮可能出錯的邊界條件,把測試火力集中在那兒;
  • 程序公敵:積極思考如何破壞代碼的正確運行; 當事情被認爲應該會出錯時,別忘了檢查是否拋出了預期的異常;
  • 不要因爲測試無法捕捉所有bug就不寫測試 – 因爲測試的確可以捕捉大多數bug;
  • 繼承和多態會加大測試困難 – 之類多所以組合多,但儘量的測試每個類,會大大減少各種組合所造成的風險;

重新組織函數

6.1 Extract Method(提煉函數)

  • 動機:1.過長的函數(粒度越細越易被複用/複寫); 2.註釋才能理解用途的代碼(命名清晰);
  • 做法:
    • 新建函數並以其意圖(“做什麼”)命名 – 只要新命名能更好地昭示代碼意圖,即使代碼簡單也可提煉,反之,就別動;
    • 提煉局部變量 – a.無變更使用Replace Temp with Query; b.一次變更可作爲參數; c.多次變更傳參+返回值;
    • 將代碼段替換爲目標函數 → 編譯,測試;

6.2 Inline Method(內聯函數)

  • 在函數調用點插入函數本體代碼,然後刪除函數;
  • 動機:1.內部代碼和函數名同樣清晰易讀; 2.組織不甚合理的函數,內聯到大型函數後再提煉出合理的小函數;
  • 做法:確定該函數不具有多態性 → 內聯替換 → 編譯測試 → 刪除函數定義;

6.3 Inline Temp(內聯臨時變量)

  • 將所有對該變量的引用動作,替換爲對它賦值的那個表達式自身; 無危害,除非妨礙了重構;

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

  • 將表達式提煉到新的獨立函數中,並替換所有引用點;此後,該函數就可被其他函數調用;
  • 動機:臨時變量驅使函數”變胖”,而函數會使代碼更清晰明瞭;

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

  • 將複雜表達式(或其中一部分)的結果放進一個臨時變量,以變量名稱來解釋表達式的用途;
  • 動機:表達式複雜而難以閱讀,尤其是”條件邏輯”; 但儘量用Extract Method來解釋一段代碼的意義;

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

  • 針對每次賦值,創造一個獨立、對應的臨時變量;
  • 動機:除了”循環變量”和”結果收集變量”,同一臨時變量承擔了兩種及以上不同責任;

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

  • 以臨時變量替換被賦值的參數; “出參數”的函數例外,但應儘量少用,多用返回值進行返回;

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

  • 因局部變量無法Extract Method,將其放入類中,局部變量作爲字段 – 即可在同一個類中將大型函數分解爲多個小函數;

6.9 Substitute Algorithm(替換算法)

  • 將函數本體替換爲另一個算法(更清晰/更簡單/效率更高);

在對象之間搬移特性

7.1 Move Method(搬移函數)

  • 在該函數最常引用/交流的類中新建一個類似函數; 將舊函數變成一個單純的委託函數,或完全移除;
  • 動機:1.一個類具有太多行爲; 2.一個類與另一個類有太多合作而高度耦合;
  • 做法:定位強關聯特性 → 分析該特性相關的字段/函數 → 整體搬移;

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(引入本地擴展)

  • 建立一個新類,使它包含這些額外函數 – 讓擴展成爲源類的子類(subclassing)或包裝類(wrapping);
  • 需要爲服務類提供額外函數,但無法修改這個類; 本地擴展 – “函數和數據應該被統一封裝”;

重新組織數據

8.1 Self Encapsulate Field(自封裝字段)

  • 爲字段建立取值/設值函數,並且只以這些函數來訪問字段;
  • 動機:1.訪問超類字段,又想在子類中將對這個字段的訪問改爲一個計算後的值; 2.延遲初始化(只在需要用到時才初始化);
  • 在有”屬性”語義的語言中,建議都用”屬性”,比如Delphi – 除了上訴優點外,同時兼顧了易於閱讀的特性[直接訪問變量的優點];

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

  • 某個數據需要與其他數據和行爲一起使用纔有意義 – 將數據項變成對象;

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

  • 從一個類衍生出許多彼此相等的實例,希望將它們替換爲同一對象 – 將這個值對象變成引用對象;
  • 動機:希望給某個對象加入一些可修改數據,並確保對任何一個對象的修改都能影響到所有引用此對象的地方;
  • 做法:
    • 1.Replace Constructor with Factory Method → 編譯測試;
    • 2.決定訪問新對象的途徑 – a.一個靜態字典或註冊表對象; b.也可多個對象作爲訪問點;
    • 3.決定這些對象的創建方式 – a.預先創建(從內存中讀取,得確保在被需要的時候能被及時加載); b.動態創建;
    • 4.修改工廠函數使其返回引用對象 – a.預先創建,需考慮萬一所求一個不存在的對象應如何處理; b.命名要體現出返回的是既存對象;

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

  • 有一個引用對象,很小且不可變也不易管理 – 將它編程一個值對象;
  • 動機:併發系統中,”不可變”的值對象特別有用,無需考慮同步問題;
    • 不可變 – 比如薪資用Money類(幣種+金額)表示,若需更改薪資,應用另一Money對象取代,而非在現有Money對象上修改; 薪資和Money對象間的關係可改變,但Money對象自身不能改變;

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

  • 有一個數組中的元素各自代表不同的東西 – 以對象替換數組;對於數組中的某個元素,以一個字段來表示;

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

  • 將數據複製到一個領域對象中;建立一個Observe模式,用以同步領域對象和GUI對象內的重複數據;
  • 動機:一些領域數據置身與GUI控件中,而領域函數需要訪問這些數據;
    • 用戶界面和業務邏輯分離 – MVC(Model-View-Controller 模型-視圖-控制器),多層系統;

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

  • 兩個類都需要使用對方特性 – 添加一個反向指針,並使修改函數能夠同時更新兩條連接;

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

  • 兩個類有雙向關聯,但如今某條關聯不再有價值 – 去除不必要的關聯;

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

  • 有一個帶有特別含義的字面數值 – 創造一個常量,根據其意義命名,替換掉字面數值;

8.10 Encapsulate Field(封裝字段)

  • 類中存在一個public字段 – 將它聲明爲private,並提供相應的訪問函數;
  • 數據隱藏 – 若其他對象修改字段,而持有對象毫無察覺,造成數據和行爲分離(壞事情!!)

8.11 Encapsulate Collection(封裝集合)

  • 某函數返回一個集合 – 讓其返回該集合的一個只讀副本,並在這個類中提供添加/移除集合元素的函數;
  • 類中常用集合(array/list/set/vector)來保存一組實例,同時提供取值/設值函數; 取值函數不應返回集合自身,預防不可預知的修改;

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

  • 爲記錄創建一個”啞”數據類,以便日後將某些字段和函數搬移到該類中;

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

  • 類中有一個數值類型碼且不影響類的行爲 – 以一個新的類替換該類型碼;
  • 不能進行類型檢測,從而大概率引起bug;

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

  • 類中有不可變的類型碼且會影響類的行爲 – 以子類取代這個類型碼;
  • 動機:宿主類中出現了”只與具備特定類型碼之對象相關”的特性;
    • 例外 – a.類型碼的值在對象生命期中發生了改變; b.某些原因使宿主類不能被繼承,使用Replace Type Code with State/Strategy;

8.15 Replace Type Code with State/Strategy(狀態/策略模式取代類型碼)

  • 做法:
    • 1.Self Encapsulate Field類型碼;
    • 2.新建一個超類,以類型碼用途命名(狀態對象) → 爲每種類型碼添加一個子類,從超類繼承;
    • 3.超類中建立一個抽象的查詢函數,用以返回類型碼 → 子類複寫並返回確切的類型碼;
    • 4.源類新建一個字段來保存狀態對象 → 將查詢動作轉發給狀態對象 → 調整設值函數,將恰當的狀態子對象賦值給”保存字段”;
    • 5.用多態取代類型碼相關的條件表達式;

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

  • 各子類的唯一差別只在”返回常量數據”的函數身上 – 修改這些函數,使其返回超類中的某個(新增)字段,然後銷燬子類;
  • 動機:建立子類的目的,是爲了增加新特性或變化其行爲; 若子類只有常量函數(返回硬編碼值),則應去除子類;

簡化條件表達式

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

  • 有一複雜的條件語句 – 從if、then、else三個段落中分別提煉出獨立函數;
  • 動機:分支條件和邏輯代碼自身與代碼意圖有不小差距 – 提煉,更好的命名,會看上去如註釋版清晰明瞭;

9.2 Consolidate Condition Expression(合併表達式)

  • 將多個條件合併爲一個條件表達式,並將其提煉成一個獨立函數;
  • 動機:有一串各不相同的檢查條件但最終行爲是一致的,且各條件無其他副作用 – 目的是a.使檢查用意清晰; b.爲提煉函數做準備;

9.3 Consolidate Duplicate Condition Fragments(合併重複的條件代碼)

  • 在一組條件表達式的所有分支上都執行了某段相同的代碼 – 將這段重複代碼搬移到條件表達式之外(起始處/尾端);

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

  • 在一系列布爾表達式中,某變臉帶有”控制標記”的作用 – 以break/return語句取代控制標記;

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

  • 函數中的條件邏輯使人難以看清正常的執行路徑 – 使用衛語句(單獨的檢查)表現所有特殊情況;
  • 動機:1.所有分支都是正常行爲(if…else…); 2.分支中只有一種是正常行爲,其他則非常罕見(單獨if…語句即衛語句/條件反轉);

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

  • 條件表達式根據對象類型的不同而選擇不同的行爲 – 將原始函數聲明爲抽象函數而後將每個分支放進子類複寫函數中;

9.7 Introduce Null Object(引入Null對象)

  • 需要再三檢查某對象是否Null – 將Null值替換爲Null對象;
  • 做法:1.行爲都一致的Null行爲 – Singleton(單例模式); 2.有着特殊行爲 – Special Case(特例類);

9.8 Introduce Assertion(引入斷言)

  • 某段代碼需要對程序狀態做出某種假設 – 以斷言(交流與調試的輔助)明確表現這種假設;
  • 程序不犯錯,斷言就不會造成任何影響 – 不要濫用斷言,請只使用它來檢查”一定必須爲真”的條件;

簡化函數調用

10.1 Rename Method(函數改名)

  • 函數的名稱未能揭示其用途 – 修改函數名稱;
  • 將就可用的命名是惡魔的召喚,是通向混亂之路,千萬不要!!!

10.2 Add Parameter(添加參數)

  • 函數需要從調用端得到更多信息 – 添加參數讓其帶着所需信息;

10.3 Remove Parameter(移除參數)

  • 函數本地不再需要某個參數 – 去除參數;

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

  • 函數既返回對象狀態值,又修改對象值 – 建立兩個不同函數,一個負責查詢,一個負責修改;
  • 併發時,建立第三個函數(查詢-修改),聲明爲synchronized,裏面調用各自獨立的查詢和修改函數;

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(隱藏函數)

  • 類中某個函數,從未被其他任何類用到 – 將函數降爲private;

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

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

10.13 Encapsulate Downcast(封裝向下轉型)

  • 某個函數返回的對象,需要由函數調用者執行向下轉型 – 將向下轉型移到函數中;

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

  • 某個函數返回一個特定的代碼,用以表述某種錯誤情況 – 改用異常(只應該用於異常的、罕見的行爲);
  • 非受控異常 – 需調用者檢查,編程錯誤; 受控異常 – 被調用函數進行檢查,拋出異常,上層調用者catch並處理;

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(以繼承取代委託)

  • 在兩個類中使用委託關係,並經常爲整個接口編寫許多極簡單的委託函數 – 讓委託類繼承受託類;
  • 告誡:1.若未使用委託類的所有函數,則不應繼承; 2.受託對象被不止一個其他對象共享,且受託對象是可變的,不應繼承;

大型重構

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

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

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

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

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

  • [MVC]某些GUI類之中包含了領域邏輯 – 將領域邏輯分離出來,爲它們建立獨立的領域類;

Extract Hierarchy(提煉繼承體系)

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

總結

  1. 隨時挑選一個目標:某個地方代碼發臭了,就將問題解決掉,達成目標停止 – 不是去探索真善美,而是防止程序散亂;
  2. 沒把握就停下來:無法保證做出的更改能保持程序原本的語義 – 有改善就發佈,沒有就撤銷修改;
  3. 學習原路返回:重構後運行測試,若失敗,退回原點重新開始 – 調試時間可能很短也可能很長,但再次重複重構很快;
  4. 二重奏:結對編程,也利於重構;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章