# 大話重構
Part2 實踐篇
###### 閱讀人:薛威明

###### [重點整理](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. 抽取方法後
----
### 開發票業務演化過程

----
BUT
開發票 統計 當月 發票量 & 金額
= 財會統計類別職責
---
## 職責驅動設計
類別和介面絕不做跟自己職責無關的事
-> **內聚**
----
### 1. 分析領域模型
* 真實物件的屬性
* 真實物件的行為
* 真實物件之間的關係
易理解->可讀性
----
### 2. 分析業務變更原因
* 是否為引起軟體變更的同一原因
A的變更不該影響B
1. 檢查買方是否存在。與客戶管理有關
2. 開發票人是否有權限。與使用者權限有關
----
### 3. 尋找資訊專家
擁有執行該方法所需資料的物件
----
#### 全能者 -> 管理者 / 協調者


----
### 職責分不分
* 程式法複雜度
---
## 單一職責原則 (SRP)
##### Single Responsibility Principle
##### SOLID原則的第一個
```一個職責就是軟體變化的一個原因```
----
∵需求變更
∴變更原因增加
=> 類別拆分
----
### 繼承拆分
1. 劃分相同業務與不同業務
(相同程式碼與不同程式碼)
3. 分拆
* 相同業務 -> 抽象父類別
* 不同業務 -> 實作子類別
----


---
## 小節
* 適時整理程式碼 - 單一職責原理 - 減少超級大物件
* 面對超級大物件 - 小步快跑 - 無法一次釐清
* 合久必分 - 先用方法抽象類別
* 分久必合 - 久了熟悉繪製領域模型
----
超級大物件產生

----
合久必分 - 先依方法抽類別

----
分久必合 - 領域模型清晰

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)) {
//按頁匯出的程式碼
}
```
----
#### 可擴展

----
```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()
----
新需求:時間範圍選單

新增函式:transform()空函式在抽象類別
新增類別:MonthRangeControl複寫函式transform()
----

---
## 新問題:設計之初不確定性多,業務邏輯前後充滿檢驗
需考慮很多,反而不能集中精神業務邏輯分析
----
### 暫時先不去處理,但是設計擴展點出來
發展前期不用權限檢查,後期再加
最初處理量小,後期處理量可能大
----
### 頗面導向程式設計
= 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 匯出報表類別
客戶端使用介面,透過工廠模式注入實作類別

----
#### 範例2 表單操作類別

---
#### 個人白話見解
* 依賴反轉原則 降低與實作類別耦合,讓類別成員好替換
* 工廠模式 降低選擇實作類別的耦合程度,減少參與物件生成過程
---
## 適配器模式 解偶 外部介面
* Adapter:不相容的界面接在一起
* 應付plugin升級/替換
----
### 書本案例 (財務系統切換)
1. 初始內部系統可能直接只用天心財務軟體,但發現鼎新的窗口不相容
2. 定義出財務系統介面,實作使用天心的類別(直接對接)
3. 實作使用鼎新的類別,裡面會處理很多轉換程式

----
### 遊戲舉例 (變體角色)
1. 角色系統原本類別有我方/敵方
2. 現在新增要把敵方變為我方寵物

---
## 橋接模式 解決 繼承氾濫
* Bridge:多種變化形成繼承,改寫為組合 (M x N -> M + N)
* 拆出變因 -> 多元組合
----
### 書本案例 (資料庫讀取各式資料)
* 避免不同的資料庫對應不同的資料格式讓類別以乘法增長
* 而是改為注入不同處理資料格式物件,讓類別以加法增長

----
### 遊戲案例 (角色與武器)
* 如果繼承就會實作:GunKight、GunOrge、RifleKight、RifleOrge、RocketKight、RocketOrge
* 角色一多、武器一多,就會繼承氾濫

---
## 策略模式 解耦 方法
----
### 書本案例 (員工薪水)
* 例:員工底薪、業務獎金、經理抽成
* 但是員工身分不固定,可能同時身兼多份身分


---
## 命令模式 解耦 程序
---
### 書本案例 (SQL使用)
* 拉出共同介面,各個步驟繼承 -> 命令類別
* 建立一個Processor自由串列這些命令
> 例:Find = PreCompiler -> Parametric -> SqlBuilder -> Paging -> GroupBy -> OrderBy
> 例:Count = PreCompiler -> Parametric -> SqlBuilder -> Count
* 當有新需求可以建立新命令插入或更換
----

---
## 透明功能擴展
客戶程式完全不知道功能擴展
----
### 組合模式 & 裝飾者模式
* 組合模式
* 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.領域驅動設計
---
重構 ≠ 重新開發
---
[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}]"}