重構,第一個案例
1.1 起點
- 如果發現現有的代碼結構使你無法很方便地添加新特性,那就先重構,使特性的添加比較容易進行後,再添加特性;
1.2 重構的第一步
- 爲即將修改的代碼建立可靠的測試環境 – 是人就會犯錯,所以需要可靠的測試;
- 測試結果能夠自我檢驗 – 成功”OK”,失敗列出失敗清單並打印行號 (自動化對比測試結果是提高效率的前提);
1.3 分解並重組”巨型”函數
- 切分提煉長函數(Extract Method),並移至更合適的類(Move Method) – 代碼塊越小,越容易管理;
- 重構技術 – 以微小的步伐修改程序,如果犯錯,很容易便可發現它;
- 變量重命名 – 代碼應表現自己的目的,而變量名是關鍵; 唯有寫出人類容易理解的代碼,纔是優秀的程序員;
- 去除臨時變量 – 臨時變量可能成爲問題,因爲它們只在所屬函數有效,從而助長冗長而複雜的函數;
- 重構的節奏 – 測試 → 小修改 → 測試 → 小修改 → …… , 正是這種節奏讓重構快速而安全地前進;
重構原則
2.1 何謂重構
- 調整代碼內部結構,在不改變功能的前提下,使其更易理解和修改;
- 兩頂帽子 – 添加新功能、重構:
- 添加新功能時 – 不應修改既有代碼,只管添加並通過測試;
- 重構時 – 只管改進程序結構,且只在絕對必要(接口變化)時才修改測試;
- 開發過程中帽子經常變換 – 譬如增加功能時發現更改結構會更容易; 但無論何時,要清楚自己戴的是哪頂帽子;
2.2 爲何重構
- 改進軟件設計:維持原有設計使其便於閱讀理解避免腐敗變質; 消除重複代碼,方便未來修改;
- 使軟件更易理解:讓代碼更好地表達自己的用途 – a.方便自己以後查閱; b.協助自己理解不熟悉的代碼;
- 早期重構 – “擦掉窗戶上的污垢,使你看得更遠”;
- 幫助找到Bug:越理解代碼越容易揪出Bug; 重構能更有效的寫出健壯的代碼;
- 提高編程速度:良好的設計是維持開發速度的根本;惡劣的設計會導致更多的調試、閱讀理解和尋找重複代碼;
2.3 何時重構
- 三次法則:第一次做某件事時只管去做;第二次做類似的事會反感但還可以做;第三次再做類似的事就應該重構;
- 添加功能時:a.若重構能使我更快地理解; b.若用其他方式來設計,添加功能會簡單方便很多;
- 修補錯誤時:如果收到一份錯誤報告 – 重構代碼,因爲它沒有清晰到你能一眼看出Bug;
- 複審代碼時:閱讀代碼 → 一定程度理解 → 提出建議 → 想到點子時考慮是否可通過重構輕鬆實現 → 重構 → 對代碼獲得更高層次的認識;
- 複審者+原作者(結對編程複審) – 複審者提出建議 → 共同判斷是否能通過重構輕鬆實現 → 修改;
- 大型設計的複審,用UML示意圖展現設計並以CRC卡展示軟件情節; 和團隊進行設計複審,和個人進行代碼複審;
- 如果發現昨天的決定已不適合今天的決定,放心改變這個決定以完成今天的工作,至於明天,回頭看今天覺得幼稚,那時還可以改變你的理解;
- 希望程序(1)容易閱讀; (2)所有邏輯都只在唯一地點指定; (3)新的改動不會危及現有行爲; (4)儘可能簡單表達條件邏輯;
2.5 重構的難題
- 數據庫:在對象模型和數據庫模型之間插入一個分隔層,隔離兩個模型各自的變化;
- 不需要一開始即插入分隔層,在發現對象模型變得不穩定時再產生它;
- 修改接口:接口只有被那些“找不到,即使找到也不能修改”的代碼使用時,纔會成爲重構的障礙;
- 不要過早發佈接口 – 修改代碼所有權政策,使重構更順暢; (如果非要更改已發佈接口,讓舊接口調用新接口,並標記爲Deprecate);
- 難以通過重構手法完成的設計改動:考慮候選設計方案時,對比重構難度,有限挑選更易重構的設計,即使它不能覆蓋所有潛在需求;
- 何時不該重構:a.既有代碼太混亂或錯誤過多,應該“重寫”,重構前提是代碼大部分情況下運行正常;
- b.項目已近最後期限時應避免重構;
2.6 重構與設計
- 預先設計(CRC卡等方式檢驗各種想法) → 擇一可接受方案 → 編碼 → 重構;
- 仍需思考潛在變化和靈活的解決方案,但不必逐一實現,而是問“簡單方案重構成該靈活方案有多大難度”,如果”容易”那實現目前的簡單方案即可;
- 哪怕你完全瞭解系統,也請實際度量它的性能,不要臆測,臆測會讓你學到一些東西,但十有八九你是錯的;
2.7 重構與性能
- 首先寫出可調的軟件,然後調整以獲得足夠的速度;
- 三種追求性能的編寫方法:
- 時間預算法 – 分解設計時就做好預算,給每個組件分配一定資源–包括時間和執行軌跡;每個組件絕對不能超過自己的預算;
- 持續關注法 – 任何時候做任何事時,都要設法保持系統的高性能; 作用不大,因爲過於分散,視角也和狹隘;
- 關注性能熱點 – 90%的時間都花在了10%的代碼上; 使用度量工具監控程序的運行,讓它指出耗時/耗空間的熱點代碼,謹慎修改並多多測試以調優代碼;
代碼的壞味道
3.1 重複代碼(Duplicated Code)
- 如果在一個以上的地點看到相同的程序結構 – 設法將他們合而爲一;
- 同一個類的兩個函數含有相同的表達式; (Extract Method)
- 兩個互爲兄弟的子類內含相同表達式; (Extract Method → Pull Up Method(推入超類) → Form Template Method(提煉相似分割差異))
3.2 過長函數(Long Method)
- 擁有短函數的對象會活得比較好、比較長 – “間接層”能帶來的諸如解釋、共享、選擇能力都是由小型函數支持的;
- 一個好名字 – 讓小函數容易理解的真正關鍵,直觀瞭解而無需進入其中查看;
- 更積極地去分解函數:
- 每當感覺需要註釋來說明某塊代碼時,就把其放入一個獨立函數中,並以其用途(而非實現手法)命名;
- 條件表達式和循環也是提煉的信號 – Decompose Condition處理條件表達式;
- 消除大量的參數和臨時變量 – Replace Temp With Query消除臨時元素; Introduce Parameter Object / Preserve Whole Object將過長參數變得簡潔;
3.3 過大的類(Large Class)
- 太多成員變量 – 將類中相同前綴或字尾的成員變量提煉到某個組件內,如果該組件適合作爲一個子類,可用Extract Subclass;
- 太多代碼 – 有多個相同代碼則提取成函數; 或將部分代碼提取成類/子類來瘦身;
- GUI大類 – 把數據和行爲移到獨立的領域對象去,可能需要兩邊各保留一些重複數據並保持兩邊同步(Duplicate Observed Data);
3.4 過長參數列(Long Parameter List)
- 太多參數會造成函數不易使用,且一旦需要更多數據就不得不修改它; 如果傳遞對象,只需(在函數內)增加一兩條請求就能得到更多的數據了;
- Replace Parameter with Method – 若向已有對象(函數所屬類/另一參數)發出請求即可取代一個參數,則激活此重構手法;
- Preserve Whole Object – 將來自同一對象的一堆數據(參數)收集起來,並以該對象替換它們;
- Introduce Parameter Object – 若某些數據缺乏合理的對象歸屬,爲它們製造出一個”參數對象”;
3.5 發散式變化(Divergent Change)
- 一旦需要修改,只需跳到系統某處並只在此處修改 – 不行,則應考慮重構;
- 若某個類經常因爲不同的原因在不同的方向上發生變化 – 找出特定原因造成的變化,Extract Class將它們提煉到另一個類中;
3.6 霰彈式修改(Shotgun Surgery)
- 若每遇到某種變化,都必須在許多不同的類內做出許多小修改 – 需使”外界變化”與”需要修改的類”趨於一一對應;
- Move Method和Move Field把需要修改的代碼放進同一個類; 若無合適的類安置這些代碼,就創造一個(Inline Class);
3.7 依戀情節(Feature Envy)
- 函數對某個類的興趣高過對自己所處類的興趣 – 移到該去的地方
- 函數用到好幾個類的功能 – 哪個類擁有最多被此函數使用的數據,就把該函數置於哪(先分解後移動);
3.8 數據泥團(Data Clumps)
- 那些總是綁在一起出現的數據,當刪掉其中某項,其他數據因而失去意義時 – 你應爲它們產生一個新對象;
- 一旦提煉出新對象,即可着手尋找Feature Envy(3.7),分解,提煉,不必太久,所有的類都將充分發揮價值;
3.9 基本類型偏執(Primitive Obsession)
- 多多運用小對象 – 譬如money類(幣值+幣種)、range類(起始值+結束值)等等;
3.10 switch驚悚現身(Switch/Case Statements)
- 多數情況下,看到switch/case語句,應考慮”多態”來替代;
- 若只是在單一函數中有些選擇事例,用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(提煉繼承體系)
- 某個類做了太多工作,其中部分工作是以大量條件表達式完成的 – 建立繼承體系,以一個子類表示一種特殊情況;
總結
- 隨時挑選一個目標:某個地方代碼發臭了,就將問題解決掉,達成目標停止 – 不是去探索真善美,而是防止程序散亂;
- 沒把握就停下來:無法保證做出的更改能保持程序原本的語義 – 有改善就發佈,沒有就撤銷修改;
- 學習原路返回:重構後運行測試,若失敗,退回原點重新開始 – 調試時間可能很短也可能很長,但再次重複重構很快;
- 二重奏:結對編程,也利於重構;