Legacy Code 重構實戰練習 I(上) — DDD 讀書會筆記 Ch01 — Ch05

Nick
10 min readMar 4, 2020

前言

近期參加 DDD-TW(Domain Driven Design) 舉辦的 Legacy Code 讀書會,要讀 Working Effectively with Legacy Code 這本書,想透過文章紀錄一下。

這次導讀進度為章節 1 ~ 5,以下會把筆記以問答方式做個小整理,大家可以試著在心中回答一次,再往下看筆記回答,本篇最後把導讀者提供非常棒的「重構練習範例」附上討論紀錄,且會選用 javascript 實作來分享。

導讀小筆記

Q1:調整程式碼起因,你能分辨嗎:增加特性、修正Bug、重構、最佳化嗎

Q2:單元測試不牽扯 DB 讀取、IO、畫面顯示,那這還算一個有效測試嗎?

Q3:你心中修改程式碼的步驟,以及書中提到的步驟是?

Q4:Legacy Code 很難被直接測試,如何運用「感測」與「分離」

Q5:什麼是 SEAM(接縫)與 Enabling Point(致能點) 及他們的關係

A1:以下是增加特性、修正Bug、重構、最佳化差別比較圖

差異圖

A2:單元測試確實不能如 E2E Testing 完整驗證系統是否能完整運作,但它會是 CP 值最高的方法。

在編寫單元測試時,我們的採用「分而治之,逐一擊破」的方案以輕量、好讀、好懂,並能快速定位問題為目標實作。

A3:修改程式碼的五步驟:

  1. 確認變動點:需求修改位置
  2. 找出測試點:測試對象可涵蓋變動點即可
  3. 解依賴:本書大重點
  4. 編寫測試:實作單元程式
  5. 修改、重構

A4:如何運用「感測」與「分離」

感測:當無法存取程式碼計算值,可以透過解依賴來「感測」這些值,比如假人測試,用假人測試車禍撞擊的傷害程度。

分離:當我們無法特別針對部分程式碼進行測試,就需要透過幾解依賴將程式碼「分離」

其實上面講的兩個都可以對應到單元測試講到的「Mock 跟 Stub」功能,只是在 Legacy 上可能有更多實務的做法。

如以下是一個小範例,Sale 會使用到 scan 去讀條碼資訊,然後顯示在收營機畫面上。如果想要測試 scan 這個單元有沒有成功吐出正確的資訊就會面臨到無法「感測」讀值及無法「分離」顯示數值到螢幕的問題。

class Sale{
scan(barcode){
 ...
itemLine = item.name();
 ...
viewer.showLine(itemLine);
}
}

調整後將螢幕物件改成在 Sale 初始化的時候就傳入,這樣在 testing 程式可以自行傳入 Fake Object(偽物件),再定義這個物件決定要被誰「感測」,也因此「分離」了計算及顯示兩個邏輯、解除之間的依賴。

class Sale{
constructor(viewer) {
this.viewer = viewer;
}
scan(barcode){
...
itemLine = item.name();
...
this.viewer.showLine(itemLine);
}
}

A5:SEAM(接縫)與 Enabling Point(致能點)

SEAM(接縫):程式中特殊點,在這些底上無需做任何修改,就可以達到變動程式的目的。
Enabling Point(致能點):每個接縫都有一個智能點,在智能點可以決定接縫的行為。

環境接縫:用 config 切換

  • JAVA:classpath
  • NodeJS:Env files for testing, dev
  • K8s config map files for different env

物件接縫:不事先指定哪一個類別會被呼叫,如上個問答的範例 Sale 使用的顯示器即是一個 接縫 及 致能點。

class Sale{
constructor(viewer) {
// 致能點
this.viewer = viewer;
}
scan(barcode){
...
itemLine = item.name();
...
// 物件接縫
this.viewer.showLine(itemLine);
}
}

案例實作

這此選用到 FongX777/trip-service-kata 這個經典 Legacy Code 的練習專案,讓現場會中花一小時練習,當中有各式語言給大家挑選,如 javascript、java、c++、c#、python、php、ruby … ,那我們改使用 javascript 來試一次吧。

目標:為 TripService.js 這個 legacy code 進行重構

在重構前只有一個條件,就是確保原有功能有被測試覆蓋,以下先來了解這個 Service 要重構的功能,才能知道要怎麼覆蓋

在這個 Service 只有一個 method 叫做 getTripsByUser,是一個可以抓取旅行資訊的功能,當使用者是登入且是名單上的人的話就可以取得旅遊資訊,可以將這功能拆成三大塊,如以下

  1. shouldThrowExceptionWhenUserIsNotLoggedIn 使用者未登入時會跳 Exception
  2. shouldNotReturnTripsWhenLoggedUserIsNotAFriend 當使用者不是朋友時不會回傳任何結果
  3. shouldReturnTripsWhenLoggedUserIsAFriend 正常執行,回傳使用者旅行
Demo Legacy Code — TripService
├── src
│ ├── Trip.js
│ ├── TripDAO.js
│ ├── TripService.js
│ ├── User.js
│ └── UserSession.js
└── test
└── TripServiceSpec.js

下載專案並執行測試

git clone https://github.com/FongX777/trip-service-kata.gitcd ./trip-service-kata/javascriptnpm installnpm run test

以下將依序撰寫三個功能的覆蓋測試,最後再來討論重構內容。

測試覆蓋 shouldThrowExceptionWhenUserIsNotLoggedIn

將使用者是被認定是未登入情況下,要 throw exception

第一題就卡關

想要測試 throw new Error(‘ User not logged in.’) 這段,馬上就會遇到這邊的控制條件是 UserSession 裡面的 getLoggedUser 回傳要是 null ,而 UserSession 又是一個 global variable !!

別怕這時候就可以用上面筆記提到的「分離」、「製造接縫」的方式,但這邊希望先針對解除耦合讓測試可跑為目的,可以改成以下。

製造接縫(SEAM)

那該怎麼製作 Enabling Point(致能點)呢,這邊比較 Tricky 一點,要直接 override 這個 getLoggerUser

在測試建立一個 UnderTest Service 去 override getLoggerUser
測試覆蓋成功

測試覆蓋 shouldNotReturnTripsWhenLoggedUserIsNotAFriend

如果這位登入的使用者不是在朋友名單內,就要回傳空陣列,需要執行到這個情境需要符合兩個條件。

  • 是已登入的會員 loggedUser 不能是 null
  • loggedUser 值不能在 friends 名單內(由 user getFriends 取得 firends)
目前主程式
User.js

看來這個測試的執行條件還算好寫

  • loggedUser :這個變數剛在前個測試已經有 override 可控制,只是要改成可以動態依照不同 test 進行設定
  • user :這個變數是在 method 時提供的參數,符合物件接縫的格式,可以直接在 test 時替換,也觀察 getFirends 可以怎麼使用,看了知道如果都不設定去 create User 會是空 firends 名單,那就也不會判斷到任何用戶是 friends,但為了要測試記得要在 trips 加一點內容來確定實際真的是取到預設值得空陣列。
  • (建議也加)TripDAO:因如果錯誤執行時會執行到 TripDAO 這個 global variable ,要確保這個回傳不是空陣列,才能驗證是不是真的沒執行到 TripDAO。
Service 調整
新增測試覆蓋
測試成功

測試覆蓋 shouldReturnTripsWhenLoggedUserIsAFriend

這個就是剛實作的「shouldNotReturnTripsWhenLoggedUserIsNotAFriend」的反向測試案例,完全可以直接添加測試案例即可。

測試覆蓋
測試成功

最後一步:rafatcor ( 重構 )

最後一步打包下班 X,不對呀,這次目的是重構,但是 … 寫得好累啊,下篇再來繼續重構好了 XD

小結

本篇介紹了一些名詞「感測」、「分離」、「接縫」、「致能點」,用來釐清在面對 Legacy Code (沒有被測試覆蓋的程式)如何評斷目前的問題,再用新增物件接縫的技巧解除了耦合(coupling),有了重構的第一步,也就是補上測試,而更完整的「重構」我們將在下篇一起來討論怎麼的程式碼才是更「乾淨」的,有任何問題都歡迎留言討論,謝謝你看完。

如果喜歡我這篇文,可以幫我拍手 1-10 下
如果覺得這文章對你有幫助,可以幫我拍手 10–30
如果覺得想趕快看到重構內容,可以幫我拍手 30–50
讓我知道也記得 Follow我 陳建宇
更歡迎你在下方留言,我很樂意與你討論聊天或回答問題!

參考資源

--

--

Nick

嗨 我是 Nick,目前在 ShopBack 擔任軟體工程師,喜歡用科技解決問題,偶爾會整理一些工作上的發現分享在這,歡迎大家追蹤及交流