# 大話重構 Part3 進階篇 ###### 閱讀人:薛威明 ![](https://hackmd.io/_uploads/S1xqAAfk2.jpg) ###### [重點整理](https://hackmd.io/6aH8UWWQTdanAcNcYp4JlQ)、[投影片影片](https://www.youtube.com/playlist?list=PLDE-E73wU5urgMZvpCEUhbvYvq1-n1Z0Z)、[博客來連結](https://www.books.com.tw/products/0010687580) --- # Ch12 什麼時候重構 --- ## 重構是一種習慣 * 程式可讀性增高 * 程式複用性提高 * 與敏捷開發相輔相成 --- ## 兩頂帽子 先重構再擴展 * ∵新功能加入 -> 系統品質下降 -> 重構 / 重頭做 * 務必先重構讓系統維持品質,遵循OCP原則 --- ## 緊急任務時 又如何重構 時間不夠、範圍又太深太廣 ---- ### 做完整的重構設計 ---- ### 只完成最緊急的部分 * 紀錄未完成開發部分 * 紀錄其他沒時間處理的重複部分 ---- #### 遏制糟糕設計蔓延 --- # CH13 測試驅動開發 --- * TDD = Test-Driven Development * 極限程式設計的重要部分 * XP = eXtreme Programming --- * 同一次開發,不必刻意遵循OCP原則 * 針對遺留系統重點測試即可(範圍限縮) * 經常維護 * 複雜度高 * 易出錯 * 建立靜態類別圖分析關係 --- ## 個人淺見 * 測試是重構的保險 * 先寫測試後寫測試都比沒有好 * 易出錯、複雜的地方盡量佈滿測試 --- # CH14 # 全面的升級任務 --- ## 計畫式設計 vs 演進式設計 * 計畫式設計 = 瀑布式開發。 * 更動規則須寫在合約 * 閉門造車,風險最後顯現 * 計畫趕不上變化 * 演進式設計 = 反覆式開發。 * 搭配自動化測試、系統重構 * 走一步算一步,缺乏長遠的規劃 ---- ## 恰如其分規劃的演進式開發 * 個人見解 * 大架構粗略的計畫式設計 * 頻繁的演進式設計,每次恰又是小架構的稍縝密計畫式設計 * 為敏捷訂好時程目標,每次敏捷就是小瀑布開發 --- ## 風險驅動設計 -> 全面升級 1. 採集和識別風險 2. 每個風險制定解決方案 3. 評估優先順序 4. 有計畫的一步一步改進 --- # CH15 # 我們怎麼擁抱變化 --- ## 領域模型分析方法 * 領域模型 * 學習、掌握業務領域的規則 * 圖形化抽象模型 * 一系列類別圖 * 誰來繪製?整個團隊討論 * 分類 1. 原文分析法 2. 領域驅動設計 --- ### 1. 原文分析法 Textual Analysis ---- #### 1.1. 產生案例 * 觸發事件 * 前置條件 * 事件流 * 基流 * 分支流 * 替代流 * 後置條件 ---- ![未命名 -2.jpg](https://hackmd.io/_uploads/SyJtNpKXp.jpg) ---- #### 1.2. 分析事件流文字描述 * 提取名詞 * 刪除名詞 (不符合核心領域) ---- #### 1.3. 轉換名詞 * 類別 * 類別的屬性 ---- #### 1.4. 原文動詞分析 * 類別的方法 * 類別的關聯 ---- #### 個人見解 * ~UML開發設計 * User Case -> Class Diagram * Other UML Diagram --- ### 2. 領域驅動設計 Domain-Driven Design DDD ---- #### v.s. 原文分析 * 可以客戶參與建模,一起形成統一語言 * 混和語言 = 軟體技術元素 + 業務領域術語 * 圖形化模型 * 類別圖方式描述問題 * 業務視角繪製 ---- #### 範例 ---- #### 客戶: * 考核系統是由許多個考核指標組成的 * 每個考核指標代表某項工作的完成情況 指標中有 1. 分母數:時間段應完成的工作量 2. 分子數:時間段正確完成的工作量 3. 過錯數:那些錯誤或沒有按時完成的工作量。 ---- #### 需求分析員: 為什麼是分子分母? #### 客戶: 因為最後要計算**正確率** 用正確率考核單位完成工作的情況 * 畫出考核指標類別,屬性寫下分母數、分子數、過錯數、正確率 ---- #### 需求分析員: 每個考核指標都有一個過錯判斷標準,是嗎? #### 客戶: * 當然,每個考核指標都有它的**過錯判斷標準**。 * 一個考核指標可能會有多個**過錯行為** * 每一個過錯行為都有各自的過錯判斷標準 * 任何一個錯了,這個**執法行為**就算過錯啦。 ---- #### 需求分析員: 先等等,剛才提到執法行為。 執法行為和考核指標是什麼關係? #### 客戶: 哦,執法行為嘛 就是執法人員對某個使用者執行的一次業務操作 考核指標中 分母數是所有執法行為個數 分子數就是正確的執法個數 過錯數就是錯誤的執法個數 ---- #### 草圖繪製 ![大話重構1](https://hackmd.io/_uploads/rkfJEsZE6.png) ##### 書中關係圖有誤 ---- #### 客戶: (看了這個草圖有些不太明白) 這個過錯類型是什麼東西? #### 需求分析員: 過錯類型就是某種類型的過錯行為呀 感覺我們在討論過錯行為的時候似乎有兩重含義 1. 某種過錯類型 2. 這個過錯類型下的具體過錯行為 為了區分這兩重含義 我把它分為過錯類型和過錯行為啦 ---- #### 客戶: 哦,不錯不錯。 嗯,這兩個箭頭怎麼跟其他箭頭不一樣 後面還跟了個菱形框? #### 需求分析員: 哦,這代表的是包含關係 表示一個考核指標可以包含多個類型的過錯行為 ---- #### 利用統一語言與客戶交流 ![大話重構1](https://hackmd.io/_uploads/rkfJEsZE6.png) ---- 執法行為與過錯行為描述為一對多的包含關係 透過對大量的考核指標具體需求的分析 發現事情其實並非如此簡單 ---- 當考核指標只有一種過錯行為時 那非常簡單,這個過錯行為對就是對,錯就是錯 但當考核指標存在多種過錯行為時,情況就複雜了 ---- #### 1. 一個執法行為同時包含多種過錯行為 過錯行為可能同時出現在某個執法行為中 每個過錯行為就是一個考核點,錯了整個就判錯 所有的考核點都正確才判正確 eq 填寫表單,所有資料填對了才算對 ---- ![大話重構12](https://hackmd.io/_uploads/HyPJ4s-E6.png) ##### 書中關係圖有誤 ---- #### 2. 考核指標定義多個過錯行為 #### 但各過錯行為不相容 即每個執法行為只能對應一個過錯行為 不可能同時對應多個過錯行為 ---- ![大話重構13](https://hackmd.io/_uploads/rkjJNiWNa.png) ---- #### 持續學習的開發旅途 * 研發人員不必是專家,也不用一夜成專家 * 透過專家,需要時間學習領域知識 * 軟體隨時間進化升級 ---- #### 領域模型 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 ---- #### 範例 ![螢幕擷取畫面 2023-12-07 095816](https://hackmd.io/_uploads/H1r1DiRSa.png) ---- * 分析上常常是類別級別 * 總圈複雜度 * 平均圈複雜度 * 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轉換 ---- 13. 由類別替代類型代碼 * 使用類別或列舉取代類型代碼(string) 14. 由子類別替代類型代碼 * eq 員工類型:程式設計師、銷售員 15. 由狀態/策略模式替代類型代碼 * eq 員工類型算薪水,運用不同策略發薪 16. 替換子類別為欄位 * 繼承氾濫 * 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 小設計 -> 測試 -> 整個系統 -> 重構 * 延伸 * 敏捷開發 * 測試驅動設計 * 持續整合 * 往復式開發 --- [Part2 實踐篇 <<](https://hackmd.io/@voxar/S1efbNudDh)
{"title":"大話重構 Part3 進階篇","contributors":"[{\"id\":\"3877c546-06f0-440b-a33d-99383a2ceb45\",\"add\":8499,\"del\":581}]"}
    309 views
   Owned this note