軟體開發

重構:消除隱藏的依賴關係

不可測試的程式碼就像是沒打掃的房間,可能玩具散落一地,又或是不穿的衣服堆得亂七八糟。

想像你的每一個類別(Class)就是房間,可能有的很小容易打掃,有的很大不容易保持乾淨。隨著你的產品(Project)房子愈來愈大,房間愈來愈多,要整理乾淨就愈難。很多人甚至就不打掃放任專案慢慢發臭,輕微一點導致
bug 開趴,嚴重一點則導致開發新功能困難舉步維艱。

今天讓我們來看看一個例子 HBBeePointUtilites,它是一個用來讀取點數暫存的物件,但因為所有 method 都是實作成 class
method,所以讓這段程式碼難以測試,在這邊將示範如何用依賴注入(Dependency Injection)
隱藏的依賴關係轉成顯著的依賴關係,藉此讓這段程式碼得以測試。

隱藏在實作的外部依賴

先來看看我們怎麼使用這個物件:

查看它的實作會發現這個物件有個外部依賴 UserDefaults.standard ,糟糕的是他是一個 singleton
,在不使用其他外部測試 framework 的情況下,我們無法換掉它,也就無法測試。

從上面這段程式碼來看,你大概可以臆測到它是為了在專案全域都可以讀取點數才這樣設計,但是這樣的設計卻犧牲了程式碼的可測試性,在這樣的前提之下我們可以如何修改呢?

先設定目標:
1. 全域存取
2. 可測試

在不破壞原功能的前提下,為這個物件增加可測試性。

獨生子模式 Singleton pattern

針對 目標1: 全域存取 其實不只是 class method 可以, singleton 也做的到:

如上面這段程式碼所示,兩段程式碼只差在.shared ,只不過一個是用 class method 另一個則是 singleton ,很容易相互取代。

建構子依賴注入 DI

目標2: 可測試 比較複雜,我們可以透過依賴注入將原先隱藏在其中的依賴UserDefaults.standard 暴露至建構子當中。

接著我們開一個 property userDefault 將原本直接使用 UserDefaults.standard 轉向在建構時對外要求一個
UserDefaults

這時候你已經可以測試了

// Arrange
let fakeUserDefaults = UserDefaults(suiteName: “Test”)! fakeUserDefaults.set(1000, forKey: “BeePoints”)
let util = HBBeePointUtilites(userDefaults: fakeUserDefaults)

// Action
let result = util.getPoint() // 1000

// Assert
XCTAssert(result == 1000) // true

確定程式碼得以測試以後,才將原本的 class method 移除,最後程式碼會是這樣:

一次做一件事

需要注意的是,一次替換太多 class method 可能導致出錯,萬一手殘就會前功盡棄。所以請一次替換一個 class
method
,並且每一次完成替換前加入測試,完成後才提交 commit 。

值得一提的是 singleton 這邊的小技巧,透過依賴注入但是預設注入 UserDefaults.standard:

這樣在 Production 程式碼可以維持乾淨,像下面的程式碼這樣:

太好了,現在這個物件可測試,我們對產品的信心又提升了一點!

讓我們回顧一下步驟:

  1. 用建構子依賴注入暴露原本隱藏的依賴
  2. 用 singleton 模式讓方法全域存取
  3. 複製 class method 為 instance method
  4. 為新的 method 加入測試
  5. 移除舊的 class method

(重複 3–5 步驟直到所有 method 都可測試,並且一次只換一個 method)

結論

若實作時沒考慮到單元測試,我們理所當然的會設計出不可測試的程式碼。但是已經髒亂的房間不代表可以不管,讓我們一起捲起袖子來大掃除。

在這次簡單的例子中我想大家都能深刻瞭解,若是平常不好好打掃,最後大掃除就很累 XD

所以,寫新的程式碼時請將可測試性考慮進去,因為愈早考慮可測試性,事情就越簡單。保持房間乾淨整齊,這樣下一個來使用的人(極有可能是3個月後的自己)才會容易根據需求更改房間擺設。

我是 honestbee 的 iOS 工程師 Liyao Chen 我們下次見!

You Might Also Like

No Comments

    發表迴響