# 大話重構 Part2 實踐篇 ###### 閱讀人:薛威明 ![](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) --- # Ch5 第一步: # 從分解大函數開始 --- ## 抽取方法 -- ### 超級大函數的產生 1. 起初不複雜、邏輯清晰、易讀、易懂 2. 業務邏輯一次次變更,不停添加 3. 開始有不合理的設計 ---- ### 範例:HelloWorld ---- #### 原始 (19行) ```java= public class HelloWorld { public String sayHello(Date now, String user){ Calendar c; int h; String s = null; c = Calendar.getInstance(); c.setTime (now); h = c.get (Calendar.HOUR_OF_DAY); if (h> 6 && h<12) { s = "Good morning!"; }else if (h>=12 && h<19) { s = "Good afternoon!"; }else{ s = "Good night!"; } s = "Hi, "+user+". "+s; return s; } } ``` ---- #### 最終 (88行) ```java= /** *The Refactoring's hello-world program * @author fangang */ public class HelloWorld { /** * Say hello to everyone * @param now * @param user * @return the words what to say */ public String sayHello (Date now, long userId) { //Get database connection. try { Class.forName ("oracle.jdbc.driver. OracleDriver"); } catch (ClassNotFoundException e1) { throw new RuntimeException("No found JDBC driver"); } String url = "jdbc:oracle: thin: @localhost:1521: helloworld"; String username = "test"; String password = "testpwd"; Connection connection; try { connection = DriverManager.getConnection (url, username, password); } catch (SQLException e1) { throw new RuntimeException ("Connect database failed!"); } //Get current month, date and hour. Calendar calendar = Calendar.getInstance(); calendar.setTime (now); int hour = calendar.get (Calendar.HOUR_OF_DAY); int month = calendar.get (Calendar.MONTH); int day = calendar.get (Calendar. DAY_OF_MONTH); //Get the right words to say hello String words = null; String greetingRuleSql = "select words, month, day, hourLower," + "hourUpper from greeting_rules"; try { PreparedStatement statement = connection. prepareStatement (greetingRuleSql); ResultSet resultSet statement.executeQuery(); while (!resultSet.isLast()) { int monthOfRule = resultSet.getInt("month"); int dayOfRule = resultSet.getInt("day"); if (month==monthOfRule && day==dayOfRule) { words = resultSet.getString("words"); break; } int hourLower = resultSet.getInt("hourLower"); int hourUpper = resultSet.getInt("hourUpper"); if (hour>=hourLower && hour<hourUpper) { words = resultSet.getString("words"); break; } } if (words==null) throw new RuntimeException( "Error when searching greeting rules."); } catch (SQLException e1) { throw new RuntimeException( "Error when getting greeting rules."); } //Get user's name String user = ""; String userSql = "select name from rms user where user id=?"; try { PreparedStatement statement = connection.prepareStatement (userSql); statement.setLong (1, userId); ResultSet resultSet = statement.executeQuery(); user = resultSet.getString (1); } catch(SQLException e) { throw new RuntimeException( "Error when getting user's name."); } words = "Hi, "+user+". "+words; return words; } } ``` ---- #### ORZ.. --- ## 1-a. 分段重組+註解 * 關聯程式碼放在一起 * 相較獨立的程式編寫註解 * 變數宣告與真正使用的程式碼放在一起 --- ## 1-b. 抽取程式碼 * 抽取出的函式命名 * 重新命名多次 * getBlsj(int iCzyf) 開始理解不深 * transformDate() 最後理解是時間轉換 * O:使用者角度命名 X:開發者 * X過於專業 * 抽出的程式碼是功能內聚的 * 功能是說得清、道的明 * 只執行一個清晰的功能 ---- ### 可抽取的地方 1. 重複程式碼 2. 塊操作 * 條件敘述 * 迴圈語句 * try語句 ---- #### 範例 ```java= ... if (cmd != null && c,d.equals("chkCard")){ // 處處省略了500行 } else if (cmd != null && c,d.equals("chkIc")){ // 處處省略了300行 } else if (cmd != null && c,d.equals("chkBuffer")){ // 處處省略了1000行 } ... ``` ```java= ... if (cmd != null && c,d.equals("chkCard")){ byte[] ret = chkCard(reader); servletOutput(res, ret); } else if (cmd != null && c,d.equals("chkIc")){ byte[] ret = chkCard(reader); servletOutput(res, ret); } else if (cmd != null && c,d.equals("chkBuffer")){ byte[] ret = chkCard(reader); servletOutput(res, ret); } ... ``` --- ## 值物件 處理抽取函數與原函數的資料交換 * X長長的參數清單 * X:業務 O:純資料 * 初期雜亂無章,重構中逐漸改善 * 不建議返回值=值物件 -> 怕氾濫 * 針對傳遞值物件修改 --- # Ch6 第二步 # 分拆大物件 --- ## 抽取類別 --- ## 超級大物件的產生 1. 如同超級大函式 2. 抽取方法後 ---- ### 開發票業務演化過程 ![](https://hackmd.io/_uploads/r1jQLJZr2.png) ---- BUT 開發票 統計 當月 發票量 & 金額 = 財會統計類別職責 --- ## 職責驅動設計 類別和介面絕不做跟自己職責無關的事 -> **內聚** ---- ### 1. 分析領域模型 * 真實物件的屬性 * 真實物件的行為 * 真實物件之間的關係 易理解->可讀性 ---- ### 2. 分析業務變更原因 * 是否為引起軟體變更的同一原因 A的變更不該影響B 1. 檢查買方是否存在。與客戶管理有關 2. 開發票人是否有權限。與使用者權限有關 ---- ### 3. 尋找資訊專家 擁有執行該方法所需資料的物件 ---- #### 全能者 -> 管理者 / 協調者 ![](https://hackmd.io/_uploads/r1jQLJZr2.png) ![](https://hackmd.io/_uploads/HkM3_JbHh.png) ---- ### 職責分不分 * 程式法複雜度 --- ## 單一職責原則 (SRP) ##### Single Responsibility Principle ##### SOLID原則的第一個 ```一個職責就是軟體變化的一個原因``` ---- ∵需求變更 ∴變更原因增加 => 類別拆分 ---- ### 繼承拆分 1. 劃分相同業務與不同業務 (相同程式碼與不同程式碼) 3. 分拆 * 相同業務 -> 抽象父類別 * 不同業務 -> 實作子類別 ---- ![](https://hackmd.io/_uploads/r1jQLJZr2.png) ![](https://hackmd.io/_uploads/SygPmnWH2.png) --- ## 小節 * 適時整理程式碼 - 單一職責原理 - 減少超級大物件 * 面對超級大物件 - 小步快跑 - 無法一次釐清 * 合久必分 - 先用方法抽象類別 * 分久必合 - 久了熟悉繪製領域模型 ---- 超級大物件產生 ![](https://hackmd.io/_uploads/r1jQLJZr2.png) ---- 合久必分 - 先依方法抽類別 ![](https://hackmd.io/_uploads/SygPmnWH2.png) ---- 分久必合 - 領域模型清晰 ![](https://hackmd.io/_uploads/HkM3_JbHh.png) eq. 買方業務類別: * 買方名稱 * 買方狀態 --- # CH7 第三步: # 提高程式碼複用率 --- ## 降低重複程式碼 風險 > 超級大物件 --- ## 循序程式設計的煩惱 無精心設計就直接照需求循序開發 O:首次開發時間大大降低 X:維護成本大大提升 --- ## DRY原則 不要重複自己 Don't repeat yourself 又稱 OAOO 一次且僅一次 Once and only once ---- 旨在軟體開發中,減少重複的資訊 拷貝第一次還能容忍 拷貝第二次就該思考 ---- ## 如何識別相似相近的功能 1.同一流程某個環節採用不同方法 2.不同業務某個功能相似相近環節 3.本身就相似相近的功能 ---- ### 1.同一流程某個環節採用不同方法 eq. 交付訂單 1. 現金付款 2. 信用卡付款 3. 悠遊卡付款 4. 電匯付款 ---- ### 2.不同業務某個功能相似相近環節 eq. 填寫表單都會有事前的操作檢查 1. 應付單 2. 付款單 3. 核銷單 4. 收款單 ---- ### 3.本身就相似相近的功能 eq. ERP中的收款單及付款單 --- ## 提高程式複用的方法 1. 重複程式碼存在同一物件中 -- **抽取方法** 2. 重複程式碼存在不同物件中 -- **抽取類別** * 工具類別 * 實體類別(業務邏輯,需要實體) * 降低散彈槍修改(Shotgun Surgery) .   3. 各類別具有某種並列關係 -- **抽取父類別** 1. 整理(抽取方法) 2. 比較(相似程式碼) 3. 抽取共用(子類實作) ---- 4. 繼承氾濫 -- **轉換組合** 排列組合關係 (M x N) -> (M + N) 5. 重複程式法被割裂成碎片 -- **樣板模式** SOP程式碼 * 父類別抽取不變步驟 * 子類別實作變異步驟 --- # CH8 第四步: # 發現程式可擴展點 --- ## 可擴展點 * 滿足開放—封閉原則的系統 * 兩項帽子重構的第一步完成 ---- ### 重構的每一步驟都是節點 * 每步都會引入新Bug風險 * 版本要能回朔,降低風險 --- ## 開放—封閉原則 (OCP原則) Open-Close Principle 1. 功能擴展是開放的 新增物件、類別達到功能擴充 2. 程式碼修改是封閉的 上線的程式碼是不會更動的,除非有bug ---- ### 原有程式碼與新程式碼有效隔離 * 罪惡之源 **if語句** 類別過多職責。內聚下降、耦合上升 * 搭配參數設定檔完全解耦 X:複雜度上升、效能下降 !:最初不建議深入可擴展點 ---- #### 產生報表類別 ```csharp= String exportTypeName = (String)params.get("exportType"); if ("exportAll".equals (exportTypeName)) { //全部匯出的程式碼 } else if("exportChoosen".equals(exportTypeName)) { //按選擇匯出的程式碼 } else if("exportOnePage".equals (exportTypeName)) { //匯出本頁的程式碼 } ``` ---- #### 新增按頁匯出 ```csharp= String exportTypeName = (String)params.get("exportType"); if ("exportAll".equals(exportTypeName)) { //全部匯出的程式碼 } else if("exportChoosen".equals(exportTypeName)) { //按選擇匯出的程式碼 } else if("exportOnePage".equals (exportTypeName)) { //匯出本頁的程式碼 } else if("exportPageRange".equals (exportTypeName)) { //按頁匯出的程式碼 } ``` ---- #### 可擴展 ![](https://hackmd.io/_uploads/BJc0RXs82.png) ---- ```csharp= String exportTypeName = (String)params.get("exportType"); If ("exportAll".equals(exportTypeName)) { //全部匯出的程式碼 } else if ("exportChoosen".equals(exportTypeName)) { //按選擇匯出的程式碼 } else if ("exportOnePage".equals(exportTypeName)) { //匯出本頁的程式碼 } else if ("exportPageRange".equals(exportTypeName)) { //按頁匯出的程式碼 Exporter exporter = new ExportPageRange (); exporter.doExport (resultset); return exporter.getFileInfo(); } ``` ---- #### 利用參數檔完全解決if ```xml= <bean id="exportBus" class="com... reporter.bus.impl. ExportBus Impl"> <description>匯出資料BUs</description> <propertyname="exportTypes"> <map> <entry key="exportAll"><!-- 全部匯出 --> <bean class="com...reporter.export.ExportAll"/> </entry> <entry key="exportOnePage"><!-- 匯出本頁 --> <bean class="com...reporter.export.ExportOnePage"/> </entry> <entry key="exportChosen"><!-- 按選擇匯出 --> <bean class="com...reporter.export.ExportChosen"/> </entry> <entry key="exportPageRange"><!-- 按頁匯出 --> <bean class="com...reporter.export.ExportPageRange"/> </entry> </map> </property> </bean> ``` ---- ```java= String exportTypeName = (String)params.get("exportType"); Exporter exporter = exprotTypes.get (exportTypeName); exporter.doExport (resultset); return exporter.getFileInfo(); ``` --- ## 新問題:各實作類別程式碼覆用怎麼辦 ---- ### ANS 樣板模式 * 相同程式碼拉成抽象類別方法 * 不同程式碼抽象類別統一名稱,繼承類別各自實作 ---- #### 但是處理步驟可能會變更 * 頭尾有增加 * 中間有插入 ---- #### ANS 鉤子 (hook) 空函式放入抽象類別中 * 原有類別直接呼叫空函示,不影響既有功能 * 新類別實作 ---- #### 範例:平台控制項設計 * 文字方塊 * 下拉清單 * 單選框 * 核取方塊 * ... 原有類別:DefaultControl 原有函式:darw()、beUsed() ---- 新需求:需要先查詢資料 抽象類別:AbstractConrol 新增函式:getItems()空函式在抽象類別 新增類別:QueryControl複寫函式getItems() ---- 新需求:時間範圍選單 ![](https://hackmd.io/_uploads/By_pBYTUn.png) 新增函式:transform()空函式在抽象類別 新增類別:MonthRangeControl複寫函式transform() ---- ![](https://hackmd.io/_uploads/r1quvK6U3.png) --- ## 新問題:設計之初不確定性多,業務邏輯前後充滿檢驗 需考慮很多,反而不能集中精神業務邏輯分析 ---- ### 暫時先不去處理,但是設計擴展點出來 發展前期不用權限檢查,後期再加 最初處理量小,後期處理量可能大 ---- ### 頗面導向程式設計 = Aspect Oriented Programming = AOP * interceptor攔截器 = 擴展程式 利用參數檔加入複數功能 --- ## 小節 * 面對修改,盡可能遵守OCP * 面對重構,盡可能兩項帽子 * 面對擴展點 * 萬惡魔王if-else或是switch語句 * 拉出類別 + 設定檔 * 面對相似操作步驟 * 面板模式 * 操作前後有不確定設置檢查 * 頗面導向設計 * 其他 * 利用繼承滿足不同架構 --- # CH9 第五步: # 降低程式依賴程度 --- * 目標:各功能像插座插頭一般關係,隨意插拔 * 實作:細微面靠設計模式 --- ## 依賴反轉原則(DIP:Dependency Inversion Principle) 1. 高層次模組不應該依賴低層次模組,兩者都依賴抽象介面 2. 抽象介面不應該依賴具體實作,具體實作應該依賴抽象介面 --- * **定義介面 = 契約** * 解偶相互依賴關係 --- ## 工廠模式產生實作介面 ## 解耦 實作類別 1. 簡單工廠 Simple Factory 2. 工廠方法 Factor Method 3. 抽象工廠 Abstract Factory (詳細請見設計模式) 透過ID請工廠尋找特定類別並實作 使用者不用直接耦合實體 ---- #### 範例1 匯出報表類別 客戶端使用介面,透過工廠模式注入實作類別 ![](https://hackmd.io/_uploads/BJc0RXs82.png) ---- #### 範例2 表單操作類別 ![](https://hackmd.io/_uploads/r1quvK6U3.png) --- #### 個人白話見解 * 依賴反轉原則 降低與實作類別耦合,讓類別成員好替換 * 工廠模式 降低選擇實作類別的耦合程度,減少參與物件生成過程 --- ## 適配器模式 解偶 外部介面 * Adapter:不相容的界面接在一起 * 應付plugin升級/替換 ---- ### 書本案例 (財務系統切換) 1. 初始內部系統可能直接只用天心財務軟體,但發現鼎新的窗口不相容 2. 定義出財務系統介面,實作使用天心的類別(直接對接) 3. 實作使用鼎新的類別,裡面會處理很多轉換程式 ![](https://hackmd.io/_uploads/H1AQ-_Tpn.png) ---- ### 遊戲舉例 (變體角色) 1. 角色系統原本類別有我方/敵方 2. 現在新增要把敵方變為我方寵物 ![](https://hackmd.io/_uploads/SyaZXd6a3.png) --- ## 橋接模式 解決 繼承氾濫 * Bridge:多種變化形成繼承,改寫為組合 (M x N -> M + N) * 拆出變因 -> 多元組合 ---- ### 書本案例 (資料庫讀取各式資料) * 避免不同的資料庫對應不同的資料格式讓類別以乘法增長 * 而是改為注入不同處理資料格式物件,讓類別以加法增長 ![](https://hackmd.io/_uploads/BJy-zpRT3.png) ---- ### 遊戲案例 (角色與武器) * 如果繼承就會實作:GunKight、GunOrge、RifleKight、RifleOrge、RocketKight、RocketOrge * 角色一多、武器一多,就會繼承氾濫 ![](https://hackmd.io/_uploads/SkQomTA6n.png) --- ## 策略模式 解耦 方法 ---- ### 書本案例 (員工薪水) * 例:員工底薪、業務獎金、經理抽成 * 但是員工身分不固定,可能同時身兼多份身分 ![](https://hackmd.io/_uploads/HJnFInfAn.png) ![](https://hackmd.io/_uploads/SkbaL3GR2.png) --- ## 命令模式 解耦 程序 --- ### 書本案例 (SQL使用) * 拉出共同介面,各個步驟繼承 -> 命令類別 * 建立一個Processor自由串列這些命令 > 例:Find = PreCompiler -> Parametric -> SqlBuilder -> Paging -> GroupBy -> OrderBy > 例:Count = PreCompiler -> Parametric -> SqlBuilder -> Count * 當有新需求可以建立新命令插入或更換 ---- ![](https://hackmd.io/_uploads/SkSuo2zAh.png) --- ## 透明功能擴展 客戶程式完全不知道功能擴展 ---- ### 組合模式 & 裝飾者模式 * 組合模式 * Unity GameObject上的Compoment * 水平擴展 * 通常一對多 * 裝飾者 * 包裝者 * 鉛直擴展 * 通常一對一 ---- ![](https://hackmd.io/_uploads/SkTKUWN03.png) --- # CH10 第六步: # 我們開始分層了 宏觀角度 --- ## 大型Web系統分層結構 ![](https://hackmd.io/_uploads/ryvCNlx1p.png) ---- ### 前端介面 * 網頁 * 對象:使用者 * 展示華麗介面 * 便捷操作 ---- ### 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.領域驅動設計 --- 重構 ≠ 重新開發 --- [Part1 基礎篇 <<  ](https://hackmd.io/@voxar/BJk_3Az12) [  >> Part3 進階篇](https://hackmd.io/@voxar/S1A86bZx6)
{"metaMigratedAt":"2023-06-18T07:46:15.176Z","metaMigratedFrom":"YAML","title":"大話重構 Part2 實踐篇","breaks":true,"slideOptions":"{\"progress\":true,\"slideNumber\":true}","contributors":"[{\"id\":\"3877c546-06f0-440b-a33d-99383a2ceb45\",\"add\":13522,\"del\":486}]"}
    301 views
   Owned this note