###### tags: `讀書會` `薛威明` # 大話重構講義 筆記:薛威明  [影片](https://www.youtube.com/playlist?list=PLDE-E73wU5urgMZvpCEUhbvYvq1-n1Z0Z) [投影片-Part1 基礎篇](https://hackmd.io/@voxar/BJk_3Az12) [投影片-Part2 實踐篇](https://hackmd.io/@voxar/S1efbNudDh) [投影片-Part3 進階篇](https://hackmd.io/@voxar/S1A86bZx6) [博客來連結](https://www.books.com.tw/products/0010687580) # CH1 重構:改變既有程式碼的一劑良藥 ## 什麼是重構 嚴謹而安全的過程 保證軟體改善同時,不會引入新的Bug 1. 不改變外部行為 * 輸入輸出前後是一致的 * 貫穿重構過程的是測試 * 從手工漸漸轉為自動化 * 一開始就自動化測試是不切實際 * 測試就是系統重構的保險鎖 3. 改變內部結構,使其易於閱讀、維護和變更 ### 兩頂帽子 1. 先只重構,不新增功能,有能力適應新需求 2. 再增加新功能,滿足新需求 ### 小步快跑 vs 大格局 **每次重構週期:10min ~ 1hr** * 週期越長 = 問題複雜 = 出錯機率高 # Ch2 重構方法工具箱 重構 = 一系列的等量變換 * 就像數學算式,每行算式都是等式,可以交換 * 過程雖然改變,但等號頭尾是不變的 ### 第一次重構 - 註解、重新命名、調整次數、分行 解決: * 沒有註解 * 命名不易懂 * 沒分行,可讀性低 #### 專注業務邏輯清晰 O:方法提煉 -> 類別、介面獨立 X:簡單千行程式碼或是注釋 ### 第二次重構 - 抽取方法 解決: * 可讀性低 透過函式說明 * 內聚性不佳 一個函式處理一件事 ### 第三次重構 - 抽取類別 解決: * 內聚性不佳 類別單一原則 ### 第四次重構 - 抽取介面 解決: * 擴展性不好 適應新需求 ### 領域驅動設計 來設計/重構 **領域驅動設計**( DDD - Domain-Driven Design ) 物件導向設計的類別、方法、關聯,應當與現實世界中的事物、行為及相互關係對應起來 幫助合作夥伴好理解程式碼 ## 小結 * 程式碼還是那些程式碼 * 執行結果還是那些結果 * 但結構發生變化 * 可讀性 (函式、變數命名並加上註解) * 可維護 (重新組織函數到各自函式或物件中) * 易變更 * 實務上,簡單的需求不要「過度設計」 * 書籍附錄列出重構方法 《Refactoring: Improving the Design of Existing Code》 # Ch3 小步快跑的開發模式 **錯誤發現得越早,損失越小 大格局你傷不起** * 需重構文件 - 設計錯了誰負責 * 過去維護解決的問題都在細節設計 - 易忽略 * 結果新系統=舊系統翻版 - 已修復問題再現 * 數月的光陰,成功機率50% **專案管理就是要讓風險變小** * 驗證範圍太大 -> 驗收時間越長 -> 風險越大 不同的人重構方式不盡相同 # Ch4 保險索下的系統重構 測試是保險索,確保每次重構是正確的 如果無法保證每一步都是正確的,小步快跑也不能解決風險 ## 測試種類 1. 系統測試:透過文件,設計測試案例,往往是手工 2. 單元測試:自動化測試程式 #### 重構路上不要太早自動化測試,先手動再視狀況自動化 * 前人的程式已經難以理解、複雜、相依性過高 * 隨者重構深入,原本針對函式的測式將失效而遺棄 * 設計沒有分層,面對涉及資料庫、網路溝通,難以馬上自動化測試 ## 新需求發生的時候,測試案例就需要更新 根據兩頂帽子原則 * 執行第一頂帽子時,每次重構後,測試案例都會直接通過 * 程式碼整理 * 抽取方法 * 抽取類別 * 抽取介面 * 執行第二頂帽子時 * 實作了新需求,原本的測試案例就需要更新 * 新的需求也需要新的測試案例 # Ch5 第一步:從分解大函數開始 抽取方法 ### 超級大函數的產生 1. 起初不複雜、邏輯清晰、易讀、易懂 2. 業務邏輯一次次變更,不停添加 3. 開始有不合理的設計 ## 1-a 分段重組+註解 * 關聯程式碼放在一起 * 相較獨立的程式編寫註解 * 變數宣告與真正使用的程式碼放在一起 ## 1-b. 抽取程式碼 * 取出的函式命名 * 重新命名多次 * getBlsj(int iCzyf) 開始理解不深 * transformDate() 最後理解是時間轉換 * O:使用者角度命名 X:開發者 * X過於專業 * 抽出的程式碼是功能內聚的 * 功能是說得清、道的明 * 只執行一個清晰的功能 ### 可抽取的地方 1. 重複程式碼 2. 塊操作 * 條件敘述 * 迴圈語句 * try語句 ### 值物件 處理抽取函數與原函數的資料交換 * X長長的參數清單 * X:業務 O:純資料 * 初期雜亂無章,重構中逐漸改善 * 不建議返回值=值物件 -> 怕氾濫 * 針對傳遞值物件修改 # Ch6 第二步:分拆大物件 抽取類別 ## 超級大物件的產生 1. 如同超級大函式 2. 抽取方法後 ## 如何拆解超級大物件 1. 小步快跑,先用方法抽象類別,不要想第一次就完美重構 2. 單一職責原則:一個職責就是軟體變化的一個原因。 不同職責不同類別 繼承拆分:相同業務抽象父類別,不同業務子類別實作 ## 如何避免超級大物件 / 大函式 * 設計之初使用 * 職責驅動設計 類別和介面絕不做跟自己職責無關的事 (高內聚) * 分析領域模型 模擬真實物件的屬性、行為與物件之間關係 (易理解->可讀性高) * 新需求發生時,利用單一職責檢查業務變更原因是否增加,與原本業務是否無關,適時拆分類別 A的變更不該影響B,兩者放置不同類別 * 尋找資訊專家 擁有執行該方法所需資料的物件 ### 超級大物件從 全能者 -> 管理者 / 協調者 # Ch7 第三步:提高程式碼複用率 ## 降低重複程式碼 風險 > 超級大物件 無精心設計就直接照需求循序開發 O:首次開發時間大大降低 X:維護成本大大提升 ### DRY原則 = 不要重複自己 = Don't repeat yourself = **OAOO原則** = 一次且僅一次 = Once and only once ctrl c + ctrl v 第一次還能容忍,拷貝第二次就該思考 ### 如何識別相似相近的功能 1.同一流程某個環節採用不同方法 (eq. 多元付款的選擇) 2.不同業務某個功能相似相近環節 (eq. 各類操作都會事前檢查) 3.本身就相似相近的功能 (eq. 收款單&付款單僅是甲乙方互換而已) ### 提高程式複用的方法 1. 重複程式碼存在同一物件中 -- **抽取方法** 2. 重複程式碼存在不同物件中 -- **抽取類別** * 工具類別 * 實體類別(業務邏輯,需要實體) * 降低散彈槍修改(Shotgun Surgery) 3. 各類別具有某種並列關係 -- **抽取父類別** 1. 整理(抽取方法) 2. 比較(相似程式碼) 3. 抽取共用(子類實作) 4. 繼承氾濫 -- **轉換組合** 排列組合關係 (M x N) -> (M + N) 5. 重複程式法被割裂成碎片 -- **樣板模式** SOP程式碼 * 父類別抽取不變步驟 * 子類別實作變異步驟 # Ch8 第四步:發現程式可擴展點 ## 可擴展點 * 滿足開放—封閉原則的系統 > **開放—封閉原則 (OCP原則 - Open-Close Principle)** 1. 功能擴展是開放的 新增物件、類別達到功能擴充 2. 程式碼修改是封閉的 上線的程式碼是不會更動的,除非有bug * 兩項帽子重構的第一步完成 ### 原有程式碼與新程式碼有效隔離 * 面對修改,盡可能遵守OCP * 面對重構,盡可能兩項帽子 * 設計之初不確定性多,暫時先不去處理,但是設計擴展點出來 * **鉤子 (hook)** 空函式放入抽象類別中 * 原有類別直接呼叫空函示,不影響既有功能 * 新類別實作 * 面對擴展點 1. 萬惡魔王if-else或是switch語句 是否造成類別過多職責。內聚下降、耦合上升 * 拉出類別 + 設定檔 2. 面對相似操作步驟 * 面板模式 * 相同程式碼拉成抽象類別方法 * 不同程式碼抽象類別統一名稱,繼承類別各自實作 3. 操作前後有不確定設置檢查 * 頗面導向設計 = Aspect Oriented Programming = AOP * interceptor攔截器 = 擴展程式 利用參數檔加入複數功能 4. 其他 * 利用繼承滿足不同架構 # Ch9 第五步:降低程式依賴程度 * 目標:各功能像插座插頭一般關係,隨意插拔 * 實作:細微面靠設計模式(延伸閱讀) ## 依賴反轉原則(DIP:Dependency Inversion Principle) 1. 高層次模組不應該依賴低層次模組,兩者都依賴抽象介面 2. 抽象介面不應該依賴具體實作,具體實作應該依賴抽象介面 **定義介面 = 契約** 解偶相互依賴關係 降低與實作類別耦合,讓類別成員好替換 ### 工廠模式產生實作介面 解耦 實作類別 透過ID請工廠尋找特定類別並實作,使用者不用直接耦合實體 降低選擇實作類別的耦合程度,減少參與物件生成過程 ### 適配器模式 解偶 外部介面 * Adapter:不相容的界面接在一起 * 應付plugin升級/替換 ### 橋接模式 解決 繼承氾濫 * Bridge:多種變化形成繼承,改寫為組合 (M x N -> M + N) * 拆出變因 -> 多元組合 ### 策略模式 解耦 方法 ### 命令模式 解耦 程序 ### 透明功能擴展 * 組合模式 * Unity GameObject上的Compoment * 水平擴展 * 通常一對多 * 裝飾者 * 包裝者 * 鉛直擴展 * 通常一對一 # Ch10 第六步:我們開始分層了 ## 大型Web系統分層結構  **前端介面** * 網頁 * 對象:使用者 * 展示華麗介面 * 便捷操作 **MVC層** (Web層 / 展示層) * 後台對前端資料的處理 **BUS層** (Business Logic Layer / 業務邏輯層 / 領域層) * 真正編寫程式的地方 * 領域模型的實現 **DAO層** (Data Access Layer / 資料存取層 / 持久層) * 資料庫新增、刪除、修改、查詢 **值物件** * 所有層次統一資料傳遞 ## 領域驅動設計 * 貧血模型 (Anemic Domain Model) * 領域物件=值物件不同分層傳遞 * 大量薄型service類別 * 充血模型 (Rich Domain Model) * 領域物件擁有所有屬性+業務操作 * 龐大臃腫 ## 面對技術的變革 1. 決定引入的新技術調研 2. 遺留系統分析 * 主要/次要 * 容易/困難 3. 去除框架成本 4. 小步快跑 # CH11 一次完整的重構過程 ### 重構 ≠ 重新開發 ### 1.分解大函數 #### 抽取方法 * 註解 * 變數重新命名 * 段落整理 ### 2.分拆大物件 #### 抽取類別 * SPR原則 * 注意if-else壞味道 ### 3.提高複用率 * DRY原則 * 封裝工具類別 * 組合模式 ### 4.發現擴展點 * OCP原則 * 工廠模式 * 樣板模式 * 鉤子 ### 5.降低依賴度 * 設計模式 * 分散在前面步驟 ### 6.分層 * 要保留分層重構時間 ∵初期需求≠最終需求。 !自以為->未來風險 ### 7.領域驅動設計 # CH12 什麼時候重構 ## 重構是一種習慣 * 程式可讀性增高 * 程式複用性提高 * 與敏捷開發相輔相成 ### 兩項帽子 先重構在擴展 * ∵新功能加入 -> 系統品質下降 -> 重構 / 重頭做 * 務必先重構讓系統維持品質,遵循OCP原則 ### 緊急任務時 又如何重構 時間不夠、範圍又太深太廣 **做完整的重構設計** **只完成最緊急的部分** * 紀錄未完成開發部分 * 紀錄其他沒時間處理的重複部分 **遏制糟糕設計蔓延** # CH13 測試驅動開發 * TDD = Test-Driven Development * 極限程式設計的重要部分 * XP = eXtreme Programming * 同一次開發,不必刻意遵循OCP原則 * 針對遺留系統重點測試即可(範圍限縮) * 經常維護 * 複雜度高 * 易出錯 * 建立靜態類別圖分析關係 ## 個人淺見 * 測試是重構的保險 * 先寫測試後寫測試都比沒有好 * 易出錯、複雜的地方竟然佈滿測試 # CH14 全面的升級任務 ## 恰如其分規劃的演進式開發 ### 計畫式設計 = 瀑布式開發。 * 更動規則須寫在合約 * 閉門造車,風險最後顯現 * 計畫趕不上變化 ### 演進式設計 = 反覆式開發。 * 搭配自動化測試、系統重構 * 走一步算一步,缺乏長遠的規劃 #### 個人見解 大架構粗略的計畫式設計 頻繁的演進式設計,每次洽又是小架構的稍慎密計畫式設計 為敏捷訂好時程目標,每次敏捷就是小瀑布開發 ## 風險驅動設計 -> 全面升級 1. 採集和識別風險 2. 每個風險制定解決方案 3. 評估優先順序 4. 有計畫的一步一步改進 # CH15 我們怎麼擁抱變化 ## 領域模型分析方法 * 領域模型 * 學習、掌握業務領域的規則 * 圖形化抽象模型 * 一系列類別圖 * 誰來繪製?整個團隊討論 * 分類 1. 原文分析法 2. 領域驅動設計 ## 1. 原文分析法 Textual Analysis #### 1.1. 產生案例 * 觸發事件 * 前置條件 * 事件流 * 基流 * 分支流 * 替代流 * 後置條件  #### 1.2. 分析事件流文字描述 * 提取名詞 * 刪除名詞 (不符合核心領域) #### 1.3. 轉換名詞 * 類別 * 類別的屬性 #### 1.4. 原文動詞分析 * 類別的方法 * 類別的關聯 #### 個人見解 * ~UML開發設計 * User Case -> Class Diagram * Other UML Diagram ## 2. 領域驅動設計 Domain-Driven Design (DDD) ### v.s. 原文分析 * 可以客戶參與建模,一起形成統一語言 * 混和語言 = 軟體技術元素 + 業務領域術語 * 圖形化模型 * 類別圖方式描述問題 * 業務視角繪製 ##### 書中範例截圖15.7、15.8 (書中關係圖有誤,此為修正後)   ### 持續學習的開發旅途 * 研發人員不必是專家,也不用一夜成專家 * 透過專家,需要時間學習領域知識 * 軟體隨時間進化升級 ### 領域模型 vs 領域層 * 領域模型 * 只考慮需求 * 不考慮技術實現 * 領域層 * 屬於軟體系統的分層 * 盡可能與領域模型相似 * 考慮到技術實現的可行性 ## 遺留系統中如何應用 1. 從遺留使用者手冊開始,操作系統,認識系統 2. 記錄過程,寫案例說明 * 逐漸改善的過程 * 專注在閱讀函式,而非程式碼 * 做**閱讀用**的重構 * 很快會被遺棄,when真重構時 3. 小步快跑,小範圍實施領域模型 4. 系統運行的好好的,就不用修改 # CH16 測試的困境 ## 安全重構 * 不會帶來新Bug的重構 * 不能安全重構,永遠只能看熱鬧,無法參與其中(尤其遺留系統) * +自動化測試 ---- ### 資料庫狀態變化 1. 資料庫與被測程式解偶 * 遺留系統難馬上開始 3. 保持每次測試資料庫狀態一致 ### WEB、硬體 1. 合理剔除WEB層,再BUS層測試 * 遺留系統難馬上開始 3. 系統測試 * 模擬使用者操作 * QTE (from HP) ## 自動化測試 無法完全取代 手工測試 * 大量的Bug依然手工測試發現 * 自動化目的 * 即使內部發生變化 * 外部環境依然一致 * 結果還是正確 * 當條件被打破 1. 手工測試 2. 調整測試程式碼 3. 通過測試 ## 寫測試。開發人員? 還是測試人員? ### 開發人員寫 * 單元測試級別上與程式設計實作息息相關 * 可能會忙死 (**測試開發工程師** 新職位的誕生) * 天生樂天,很少思考什麼情況不OK,不全面 ### 測試人員寫 * 天生悲觀。測試全面,挑Bug找毛病 * 懸崖測試 * 覆蓋性測試 * 隨機測試 * 探索性測試 * 可能不善寫程式 ### 敏捷開發團隊 * 打破了開發與測試之間的牆,一起工作 * 職責不斷交替 * 但大多公司,開發就是開發,測試就是測試 ### 截長補短 * 初期由開發人員,甚至測試驅動開發 * 中期交測試人員 * 先手工測試 * 有價值的測試轉開發人員 * 自動化測試 # CH17 系統重構的評價 ## 1.超級大函數數量 * 網路參考值:10、20、50行(不算括號、註解) * JAVA工具:checkstyle、PMD、FindBugs ## 2.大物件數據 * NPM:Number of Public Methods for a class * 一個類別有多少個公共方法 * JAVA工具:checkstyle、PMD、FindBugs ## 3.相同程式碼再多處複製 * JAVA工具:checkstyle、PMD、FindBugs ## 4.低耦合、高內聚 * 内聚難測量 * 耦合可測量 ### a.圈複雜度 (cyclomatic complexity) * ~至少需要多少測試案例,才能跑遍某函式所有程式分支 * if複雜度高->該方法較高耦合與較低內聚 * 流程圖,計算公式V(G) = e - n + 2 * e:邊、n:點 * 網路建議不要超過10 * 複雜度反映判定節點上,即流程圖的區域數 * V(G) = 區域數 = 判定節點數 + 1 #### 範例  --- * 分析上常常是類別級別 * 總圈複雜度 * 平均圈複雜度 * JAVA工具:JavaNCSS、Cobertura、ckjm ### b.傳入/傳出耦合 * 傳入耦合 afferent coupling * 被import * 較有價值 * 高表示設計合理 * 傳出耦合 efferent coupling * import別人 * 評價簡單 * 高表示分拆與解偶 * 不穩定性 = 傳出耦合度 / 總耦合度 * 依賴其他類別程度 ### c.繼承 * DIT:Depth of Inheritance Tree * 繼承樹深度 * NOC:Number of Children * 繼承樹子節點的數量 * 兩指標高表示**繼承氾濫** # 附錄 重構方法 ## 1 重新組織函數 1. 抽取方法 2. 行內方法 * 抽取方法反程序 4. 行內臨時變數 * 不透過中間變數直接使用方法 5. 以查詢替換臨時變數 * 將運算邏輯抽成方法直接使用 7. 引入解釋性變數 * 複雜運算式拆斷並命名變數 6. 分解臨時變數 * 同一變數多次使用,改為創造許多個解釋性變數使用 7. 移除對參數的設定 * 方法傳入的參數不修改,而是回傳一份新的 8. 以方法物件替代方法 * 將方法拉出成為方法類別使用 9. 替換演算法 ## 2 在物件間遷移 1. 遷移方法 * 依職責把方法搬到對應的類別中 2. 遷移欄位 * 依職責把欄位搬到對應的類別中 3. 抽取類別 4. 行內類別 * 抽取類別反向操作 5. 隱藏的委託關係 * 同時使用有潛藏關係的兩類別 * 僅使用其中一個,透過其控制另一個 6. 移除中間人 * 隱藏的委託關係的反向操作 7. 引入外加的方法 * 方法多載等,使操作方法的程序簡化 8. 引入本地擴展 * 繼承類別達到某些方法類別的存取 * OCP ## 3 重新組織資料 1. 自封裝欄位 * 使用getter & setter 2. 以物件取代數值 * eq 地址 address、city、street 3. 將值物件改為物件的參考 * 提供Repository,避免大量創造物件 4. 將物件的參考改為值物件 * 避免操作上干擾,降低複雜度 5. 以物件取代陣列 * object[] -> class 6. 複製被觀察資料 7. 單向關聯變為雙向 * 員工有部門欄位 * 讓部門也有員工欄位 * 易搜尋 8. 雙向關聯變為單向 * 避免閉環 9. 特殊數字改為特定符號常數 10. 封裝欄位 11. 封裝集合物件 * add & remove 12. 將記錄替換成值物件 * xml、json轉換 14. 由類別替代類型代碼 * 使用類別或列舉取代類型代碼(string) 15. 由子類別替代類型代碼 * eq 員工類型:程式設計師、銷售員 16. 由狀態/策略模式替代類型代碼 * eq 員工類型算薪水,運用不同策略發薪 17. 替換子類別為欄位 * 繼承氾濫 * eq 男&女類別 -> 人類(性別) ## 4 簡化條件運算式 1. 分解條件運算式 * 條件式封裝方法或類別,用解釋語句命名 2. 合併條件運算式 * 返回結果相同的分支合併 3. 合併條件中重覆的部分 * 分支內重覆部分拉到條件式外 4. 移除控制標記 * 迴圈中常透過變數當Flag,將其改回判斷式 ```csharp= bool flag = true ; while( ... ) { while( flag ){ ... ... if( ... ) break ; if( ... ) flag = false ; } } ``` 5. 將巢狀條件替換為多條檢查並返回之語句 * retutn寫在各分支,而非函式尾端 6. 用多型替代條件 * 條件式程式封裝子類繼承共同父類或介面 7. 引入null物件 * 執行結果可能為null,但後續不但被操作 * 會需要一值被檢查null,不如製做null物件 8. 引入斷言 * 將複雜判斷式封裝成函式 ## 5. 簡化呼叫方法 1. 重新命名方法名稱 2. 增加參數 3. 縮減參數 4. 分離查詢與更新 * 將函式分拆成查詢函式及更新函式 5. 參數化方法 * 類別中多個方法相似,將其合併 * 建立一個參數類別囊括所有變數 * 透過參數來表達不同的地方 6. 將參數替換成明確的方法 * 利用解析參數達到執行不同功能 * 直接分拆成多個方法,解析留給外部 7. 保持物件完整 * 某物件被讀取多個屬性 * 不如整個物件被讀取 8. 將參數替換成方法 * A()->B()->C() * 不如 A( B( C() ) ) * 函式簡潔 9. 引入參數物件 * 眾多的參數包成值物件 10. 移除set * 建構時設定就不會在動的屬性 11. 隱藏方法 * 不被外部用->private 12. 將建構函數替換為工廠方法 * 工廠透過擴充決定new、clone、singleton 13. 封裝向下轉型 * 竟量將轉型封裝在函式內部 * 避免客戶程式完成 14. 將錯誤碼替換為拋出例外 * 與其回傳魔鬼數字或字串不如拋例外 * 可讀性 15. 將例外代碼替換為測試代碼 * 用判斷語句避免例外發生 ## 6. 有關繼承處理 1. 上移欄位 * 欄位移到父類 2. 上移方法 3. 上移建構函數 4. 下移方法 5. 下移欄位 6. 抽取子類別 * 擴展功能 7. 抽取父類別 8. 抽取介面 9. 摺疊繼承體 * 抽取父類別反向程序 * 父子類別差異過小 * 降低程式廣度或深度 10. 塑造樣板函數 * SOP化 * 抽取父類執行順序 * 實作子類完成不同需求 11. 將繼承替換為代理 * 解決繼承氾濫、多繼承的方法 * 取消繼承改用屬性變數參考到原本父類別 * 額外寫函式執行原本父類public和protected方法 12. 將代理替換為繼承 * 將繼承替換為代理反向程序 * 代理會造成方法過多 # 結語 ### 面對變化:兩頂帽子 ### 開發 * X 大設計 * O **小設計 -> 測試 -> 整個系統 -> 重構** #### 延伸 * 敏捷開發 * 測試驅動設計 * 持續整合 * 往復式開發
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up