# 大話重構
Part1 基礎篇
###### 閱讀人:薛威明

###### [重點整理](https://hackmd.io/@voxar/H1U0Y2T13)、[投影片影片](https://www.youtube.com/playlist?list=PLDE-E73wU5urgMZvpCEUhbvYvq1-n1Z0Z)、 [博客來連結](https://www.books.com.tw/products/0010687580)
---
# 前言
----
一套軟體,第一次被開發時,一切都十分清晰:
清晰的業務需求、清晰的設計思維、清晰的程式碼
----
經歷幾次需求變更與維護之後
一切就變得不那麼清晰了
----
隨著時間推移
軟體經歷數十次的變更與維護,情況越來越糟
最初的設計師不願再看到自己的程式碼選擇離去
----
繼任者無所適從,看不懂前人設計
每次修改如同走鋼索
----
測試人員成為唯一希望
當有新技術出現,更凸顯原有的破舊與不堪
---
## 設計之初投入更多精力
## 能解決問題嗎?
----
認為可能發生的變更從未發生
原本的設計變擺設
沒考慮到的變更卻發生
品質開始下降
----
### 交付第一個版本之後
### 客戶的需求就開始變更了
客戶使用軟體遇到不那麼簡單、
不那麼主要、不那麼核心的情況
軟體就無法處理
---
## 為何學重構
程式隨者開發仍可維持
* 可讀
* 可覆用
* 可維護
* 可擴展
---
# CH1 重構:
## 改變既有程式碼的一劑良藥
----
### 重構的兩難
----
後有追兵:
大型遺留系統,不改:面對越來越多需求,修改難度高,維護成本高,落後使用新技術的競爭者
----
前有懸岩:
改,怕出問題,若已上線,風險太大
----
* 軟體外部品質 = 企業信譽
* 軟體內部品質被漠視
----
* 程式越寫越爛 -> 效能越低 -> 結構看不懂
* 剛畢業大學生耳濡目染 -> 天真以為程式就是這樣
---
## 什麼是重構
* 嚴謹而安全的過程
* 保證軟體改善同時,不會引入新的Bug
----
1. ### 不改變外部行為的基礎上,
1. ### 改變內部結構,使其易於閱讀、維護和變更
----
### 不改變外部行為
* 輸入輸出前後是一致的
----
### 測試就是系統重構的保險鎖
* 貫穿重構過程的是測試
* 從手工漸漸轉為自動化
----
### 第一步是建立測試
* 一開始就自動化測試是不切實際
#### 遺留系統:
* 設計混亂
* 介面不清晰
* 程式依賴
---
## 小步快跑 vs 大格局
**每次重構週期:10min ~ 1hr**
* 週期越長 = 問題複雜 = 出錯機率高
----
### 兩頂帽子
1. 先只重構,不新增功能,有能力適應新需求
2. 再增加新功能,滿足新需求
---
## 重構第一步:可讀性
* 業務邏輯清晰
* O:方法提煉 -> 類別、介面獨立
* X:簡單千行程式碼或是注釋
----
讀參數檔建立物件。依據步驟先拉方法出來
```java=
public abstract class XmlBuildFactoryTemplate {
/**
* 初始化工廠。根據路徑讀取XML檔,將XML檔案中的資料裝載到工廠中
* @param path XML檔的路徑
*/
public void initFactory(String path){
//尋找XML文件,讀取資料流程
findXmlFile(path);
//解析XML文件,返回根
readXmlStream(inputStream);
//根據XML檔建立類別,放入工廠中
buildFactory(root);
}
/**
* 讀取一個XML的檔,輸出其資料流程
* @param path XML檔的路徑
* @return InputStream檔輸入流
*/
protected InputStream findXmlFile (Stringpath) {
...
}
/**
* 讀取並解析一個XML的檔輸入流,以Element的形式獲取XML的根,返回之
* @param inputStream 檔輸入流
* @return ElementXML的根
*/
protected Element readXmlStream (InputStream inputStream) {
...
}
/**
* 用「從一個XML的檔中讀取的資料」建構工廠
* @param root 從一個XML的檔中讀取的資料的根
*/
protected abstract void buildFactory (Element root);
}
```
----
未來因應不同的讀檔方式
,入不同繼承讀檔的介面

----
### 領域驅動設計
DDD - Domain-Driven Design
* 物件導向設計的類別、方法、關聯,應當與現實世界中的事物、行為及相互關係對應起來
* 幫助理解
---
# Ch2 重構方法工具箱
----
重構 = 一系列的等量變換
* 就像數學算式,每行算式都是等式,可以交換
* 過程雖然改變,但等號頭尾是不變的
---
## HelloWorld重構
----
### 原始
```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;
}
}
```
----
#### 問題
* 沒有註解
* 命名不易懂
* 沒分行,可讀性低
---
### 第一版重構
#### (註解、重新命名、調整次數、分行)
```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, String user) {
//Get current hour of day
Calendar calendar = Calendar.getInstance();
calendar.setTime (now);
int hour = calendar.get (Calendar. HOUR OF DAY);
//Get the right words to say hello
String words = null;
if (hour>=6 && hour<12) {
words = "Good afternoon!";
}else if (hour>=12 && hour<19) {
words = "Good afternoon!";
}else{
words = "Good night!";
}
words "Hi, "+user+". "+words;
return words;
}
}
```
----
#### 問題
* 內聚性不佳
一個函式同時處理時間和問候語
---
### 第二版重構
#### (抽取方法)
```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, String user) {
int hour = getHour (now);
return "Hi, "+user+". "+getSecondGreeting (hour);
}
/**
* Get current hour of day.
* @param now
* @return current hour of day
*/
private int getHour (Date now) {
Calendar calendar = Calendar.getInstance();
calendar.setTime (now);
return calendar.get (Calendar.HOUR_OF+DAY);
}
/**
* Get the second greeting.
* @param hour
* @return the second greeting
*/
private String getSecondGreeting (int hour) {
if (hour>=6 && hour<12){
return "Good morning!";
}else if(hour>=12 && hour<19){
return "Good afternoon!";
}else{
return "Good night!";
}
}
}
```
----
#### 問題
* 內聚性不佳
一個類別同時處理時間和問候語
---
### 第三次重構
#### (抽取類別)
----
#### 處理時間類別
```java=
/**
* A utility about time.
* @author fangang
*/
public class DateUtil {
/**
* Get hour of day.
* @param date
* @return hour of day
*/
public int getHour(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar.get(Calendar.HOUR_OF_DAY);
}
}
```
----
#### 處理問候語類別
```java=
/**
* All kinds of greeting.
* @author fangang
*/
public class Greeting {
/**
* Get the first greeting.
* @param user
* @return Hi, {user}.
*/
public String getFirstGreeting (String user) {
return "Hi, "+user+". ";
}
/*
*Get the second greeting.
* @param hour
* @return if in the morning, say "Good morning!",
* if in the afternoon, say "Good afternoon!",
* else say "Good night!".
*/
public String getSecondGreeting (int hour) {
if (hour>=6 && hour<12) {
return "Good morning!";
}else if (hour>=12 && hour<19) {
return "Good afternoon!";
}else{
return "Good night!";
}
}
```
----
#### HelloWorld本體
```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, String user) {
DateUtil dateUtil = new DateUtil();
int hour = dateUtil.getHour (now);
Greeting greeting = new Greeting ();
return greeting.getFirstGreeting (user) +
greeting.getSecondGreeting (hour);
}
}
```
----
#### 問題
* 擴展性不好
如果問候語要加半夜
```java=
public String getSecondGreeting (int hour) {
if (hour>=6 && hour<12) {
return "Good morning!";
}else if (hour>=12 && hour<19) {
return "Good afternoon!";
}else{
return "Good night!";
}
```
---
### 第四次重構
#### (抽取介面)
----
#### 問候語改成介面
```java=
/*
* Greeting rules interface
* @author fangang
*/
public interface GreetingRule {
/**
* @param hour
* @return whether the rule is right
*/
public boolean isRight (int hour);
/**
* @return the greeting words
*/
public String getGreeting();
}
```
----
#### 實作各問候語(morning為例)
```java=
/*
* The greeting in the morning
* @author fangang
*/
public class MorningGreeting implements GreetingRule {
/* (non-Javadoc)
* @see org.refactoring.helloworld...GreetingRule#getGreeting ()
*/
@Override
public String getGreeting() {
return "Good morning!";
}
/* (non-Javadoc)
* @see org.refactoring.helloworld...GreetingRule#isRight(int)
*/
@Override
public boolean isRight(int hour) {
if (hour>=6 && hour<12){
return true;
}
return false;
}
}
```
----
#### 改寫Greeting.getSecondGreeting()
```java=
/*
* Get the second greeting.
* @param hour
* @return if in the morning, say "Good morning!",
* if in the afternoon, say "Good afternoon!",
* if in the evening, say "Good evening!
* else, say "Good night!".
*/
public String getSecondGreeting (int hour) {
for (GreetingRule greetingRule : greetingRules) {
if(greetingRule.isRight (hour)) {
return greetingRule.getGreeting();
}
}
throw new RuntimeException("Error when greeting! ");
}
```
---
* 上述僅重構教學範例
* 實務上,簡單的需求不要「過度設計」
* 書籍附錄列出重構方法
《Refactoring: Improving the Design of Existing Code》
---
## 等量變換
* 程式碼還是那些程式碼
* 執行結果還是那些結果
--
### 但結構發生變化
* 可讀性 (函式、變數命名並加上註解)
* 可維護 (重新組織函數到各自函式或物件中)
* 易變更 (抽象、介面的提取)
---
## 重構方法的層次
1. 方法的重構
* 抽取方法
3. 物件的重構
* 抽取類別
* 抽取介面
5. 物件間的重構
6. 繼承體系間的重構
7. 組織資料的重構
8. 體系架構的重構
---
# CH3
# 小步快跑的開發模式
---
## 大格局你傷不起
* 需重構文件 : 設計錯了誰負責
* 過去維護解決的問題都在細節設計 : 易忽略
* 結果新系統=舊系統翻版 : 已修復問題再現
* 數月的光陰,成功機率50%
----
### 小步快跑
錯誤發現得越早,損失越小
----
#### 個人小結
* 驗證範圍太大 -> 驗收時間越長 -> 風險越大
* 專案管理是要讓風險變小
---
## 再次HelloWord重構
* 使用者資料透過Id從資料庫取得
* 問候語加入特殊節日(e.g. 新年、情人節)
* 支援多語系(e.g.英文、中文)
----
### 不同的人不同的重構
1. 抽取方法:
* getHour()
* getFirstGreeting()
* getSecondGreeting()
2. 抽取類別:
* GreetingToUser
* GreetingAboutTime
* DateUtil
----

---
# Ch4
# 保險索下的系統重構
---
如果無法保證每一步都是正確的
小步快跑也不能解決風險
----
測試是保險索
確保每次重構是正確的
----
重構前先製作測試
1. 系統測試:透過文件,設計測試案例,往往是手工
2. 單元測試:自動化測試程式
----
重構測試層級
1. 函式 -> 類別 -> 介面、抽象
2. 底層、DAO、BUS、Web
----
### 不要過早編寫自動化測試
隨者重構深入,原本針對函式的測式將失效而遺棄
----
#### 難以自動化測試範例一:Web
* 問:需啟動很多設備。
Request、Response、Session各自擁有許多變數難以Mock
* 解:資料結構分WEB層及BUS層
WEB僅負責資料傳遞
BUS測試業務邏輯
----
#### 難以自動化測試範例一:存取資料庫
* 問:無法控制資料庫狀態
* 解:資料結構分DAO層
整個Mock掉
驗證參數是否與預期一致
---
## 加上測試的重構
CH3 helloWorld為例
1. 程式碼整理
2. 分成getFirstGreeting()和getSecondGreeting()和GetHour()
3. GreetingToUser和GreetingAboutTime
4. DataUtil和GreetingAboutTime.getGreeting()加入新需求
5. 資料庫取得資訊
----
### 自動測試 (範例使用JUnit)
* 測試資料夾 Test/[原始資料夾路徑]
* 測試目錄 Test.[原始namespace]
* 測試類別 [原來類別名稱]Test
* 測試函式 Test開頭
----
```java=
/**
* Test for {@link org.refactoring.helloWorld.resource.HelloWorld} * @author fangang
*/
public class HelloWorldTest {
private HelloWorld helloWorld = null;
/**
* @throws java.lang.Exception
*/
@Before
public void setup() throws Exception {
helloWorld = new HelloWorld();
}
/**
* @throws java.lang.Exception
*/
@After
public void tearDown() throws Exception {
helloWorld = null;
}
...
```
----
#### 測試早安
```java=22
/**
* Test method for {@link org... HelloWorld#sayHello (java.util.Date, java.lang.String)}.
*/
@Test
public void testSayHelloInTheMorning() {
Date now = DateUtil.createDate(2014, 9, 7, 9, 23, 11);
String user = "鲍曉妹";
String result = "";
result = helloWorld.sayHello (now, user);
assertThat(result, is ("Hi, 鲍曉妹. Good morning!"));
}
```
----
#### 測試午安
```java=33
/**
* Test method for {@link org...HelloWorld#sayHello(java.util.Date, java.lang.String)}.
*/
@Test
public void testSayHelloInTheAfternoon () {
Date now = DateUtil.createDate (2014, 9, 7, 15, 7, 10);
String user = "關二鍋";
String result = "";
result = helloWorld.sayHello (now, user);
assertThat (result, is ("Hi, 關二鍋. Good afternoon!"));
}
```
----
#### 測試晚安
```java=44
/**
* Test method for {@link org...HelloWorld#sayHello(java.util.Date, java.lang.String)}.
*/
@Test
public void testSayHelloAtNight () {
Date now = DateUtil.createDate (2014, 9, 7, 21, 30, 10);
String user = "IT工程師";
String result = "";
result = helloWorld. sayHello (now, user);
assertThat (result, is ("Hi, IT工程師. Good night!"));
}
```
---
#### 第一次重構 (程式碼整理)
* 變數命名
* 分行
* 註解
直接測試通過
---
#### 第二次重構 (抽取方法)
* getFirstGreeting()
* getSecondGreeting()
* GetHour()
直接測試通過
---
#### 第三次重構 (抽取類別)
* GreetingToUser
* GreetingAboutTime
直接測試通過
---
#### 第四次重構 (兩頂帽子)
* DataUtil
* GreetingAboutTime.getGreeting()
* 加入新需求 (半夜好、新年好、情人節好)
測試失敗
testSayHelloAtNight()
----
**預期Good night!變成Good evening!**
```java
/**
* @return the greeting about time
*/
public String getGreeting () {
DateUtil dateUtil = new DateUtil (date);
int month = dateUtil.getMonth();
int day = dateUtil.getDay();
int hour = dateUtil.getHour ();
if (month= 1 && day==1) return "Happy New Year! ";
if (month==2 && day==14) return "Happy Valentine's Day! ";
if (month=3 && day==8) return "Happy Women's Day! ";
if (month==5 && day==1) return "Happy Labor Day! ";
...
if (hour>=6 && hour<12) return "Good morning!";
if (hour==12) return "Good noon! ";
if (hour>=12 && hour<19) return "Good afternoon!
if (hour>=19 && hour<22) return "Good evening! ";
return "Good night! ";
}
```
----
新需求發生的時候
測試案例就需要更新
---
### 第五次重構
### (使用者及問候語透過資料庫引入)
----
#### 難以自動化測試
* Web -> 分層,針對BUS測試業務邏輯
* 資料庫 -> Mock不驗證,指定輸入回指定定輸出
----

----
#### Test
```java=
private HelloWorld helloWorld = null;
private GreetingToUserImpl greetingToUser = null;
private GreetingAbout Time Impl greetingAboutTime = null;
private final static List<GreetingRule> GREETING_RULES = getRules();
/**
* @throws java.lang.Exception
*/
@Before
public void setUp() throws Exception {
helloWorld = new HelloWorld();
greetingToUser new GreetingToUserImpl();
greetingAboutTime = new GreetingAboutTimeImpl();
helloWorld.setGreetingToUser (greetingToUser);
helloWorld.setGreetingAboutTime (greetingAboutTime);
}
/**
* @throws java.lang.Exception
*/
@After
public void tearDown() throws Exception {
helloWorld = null;
greetingToUser = null;
greetingAboutTime = null;
}
```
----
```java=27
/**
* Test method for (@link org...HelloWorld#sayHello(java.util.Date, java.lang.String)}.
*/
@Test
public void testSayHelloInTheMorning () {
final Date now = DateUtil.createDate (2014, 9, 7, 9, 23, 11);
final long userId = 2014090701;
UserDao userDao = createMock (UserDao.class);
GreetingRuleDao greetingRuleDao = createMock (GreetingRuleDao.class);
expect (userDao.loadUser (userId)).andAnswer (new IAnswer<User>() {
@Override
public User answer() throws Throwable {
User user = new User();
user.setUserId (userId);
user.setName("鮑曉妹");
return user;
} });
expect (greetingRuleDao.findAllGreetingRules ())
.andAnswer (new IAnswer<List<GreetingRule>>(){
@Override
public List<GreetingRule> answer() throws Throwable {
return GREETING_RULES;
} });
replay (userDao);
replay(greetingRuleDao);
greetingToUser.setUserDao (userDao);
greetingAboutTime.setGreetingRuleDao (greetingRule Dao);
String result = helloWorld.sayHello(now, userId);
Assert.assertEquals("Hi, 鮑曉妹. Good morning!", result);
verify(userDao);
verify(greetingRuleDao);
}
```
---
### DAO層Mock掉
----
Mock Class
```java
UserDao userDao = createMock (UserDao.class);
GreetingRuleDao greetingRuleDao = createMock (GreetingRuleDao.class);
```
----
指定input回傳指定output
```java
final long userId = 2014090701;
expect (userDao.loadUser (userId)).andAnswer (new IAnswer<User>() {
@Override
public User answer() throws Throwable {
User user = new User();
user.setUserId (userId);
user.setName("鮑曉妹");
return user;
} });
```
----

---
### if 第五次重構沒分層的測試
```java=
Date now = DataUtil.getNow();
String user = SessionUtil.getCurrentUser (session);
HelloWorld helloWorld = new HelloWorld();
String greeting = helloWorld.sayHello (now, user);
request.setAttribute ("greeting", greeting);
```
* 不適合自動測試
* 應採用手動測試
* 重構之初通常不適合建立自動化測試
---
[>> Part2 實踐篇](https://hackmd.io/@voxar/S1efbNudDh)
{"metaMigratedAt":"2023-06-17T22:43:36.636Z","metaMigratedFrom":"YAML","title":"大話重構 Part1 基礎篇","breaks":true,"slideOptions":"{\"progress\":true,\"slideNumber\":true}","contributors":"[{\"id\":\"3877c546-06f0-440b-a33d-99383a2ceb45\",\"add\":28742,\"del\":12813}]","description":"Part1 基礎篇"}