Swift 中風味各異的依賴注入
前言
在之前的文章中,我們看了一些使用依賴注入的不同方法,以實現Swift應用中更多的解耦和可測試架構。例如, 在Swift中使用工廠的依賴注入[1]中把依賴注入和工廠模式結合起來,以及在Swift中避免使用單例[2] 中利用依賴注入取代單利。
到目前為止,我的大部分文章和例子都使用了基于初始化器的依賴注入。然而,就像大多數編程技術一樣,依賴注入有多種“風味(Flavors)”,每一種都有自己的優點和缺點。本周,讓我們來看看三種不同方式的依賴注入,以及它們如何在Swift中使用。
基于初始化器
讓我們先快速回顧一下最常見的依賴注入方式——基于初始化器的依賴注入,即對象在被初始化時應該被賦予它所需要的依賴關系。這種方式的最大好處是,它保證我們的對象擁有它們所需要的一切,以便立即開展工作。
假設我們正在構建一個從磁盤上加載文件的FileLoader。為了做到這一點,它使用了兩個依賴項——一個是系統提供的FileManager的實例,另一個是Cache。使用基于初始化器的依賴注入,可以這樣實現:
class FileLoader {
private let fileManager: FileManager
private let cache: Cache
init(fileManager: FileManager = .default,
cache: Cache = .init()) {
self.fileManager = fileManager
self.cache = cache
}
}
注意上面是如何使用默認參數的,以避免在使用單例或新實例時總是創建依賴關系。這使我們能夠在生產代碼中使用FileLoader()簡單地創建一個文件加載器,同時仍然能夠通過在測試代碼中注入模擬數據或顯式實例進行測試。
基于屬性
雖然基于初始化器的依賴注入通常很適合你自己的自定義類,但有時當你必須從系統類繼承時,它就有點難用了。一個例子是在構建視圖控制器時,特別是當你使用 XIBs 或 Storyboards 來定義它們時,因為這樣你就無法再控制你的類的初始化器了。
對于這些類型的情況,基于屬性的依賴注入可以是一個很好的選擇。與其在對象的初始化器中注入對象的依賴關系,不如在之后簡單地將其分配。這種依賴注入的方式也可以幫助你減少模板文件,特別是當有一個好的默認值不一定需要注入的時候。
讓我們來看看另一個例子——在這個例子中,我們要建立一個PhotoEditorViewController,讓用戶編輯他們庫中的一張照片。為了發揮作用,這個視圖控制器需要一個系統提供的PHPhotoLibrary類的實例(它是一個單例),以及一個我們自己的PhotoEditorEngine類的實例。為了在沒有自定義初始化器的情況下實現依賴性注入,我們可以創建兩個都有默認值的可變屬性,就像這樣:
class PhotoEditorViewController: UIViewController {
var library: PhotoLibrary = PHPhotoLibrary.shared()
var engine = PhotoEditorEngine()
}
請注意 *"通過 3 個簡單的步驟測試使用了系統單例的 Swift 代碼"*中的技術是如何通過使用協議來為系統照片庫類提供一個更抽象的PhotoLibrary接口。這將使測試和數據模擬變得更加容易!
上述做法的好處是,我們仍然可以很容易地在測試中注入模擬數據,只需重新分配視圖控制器的屬性:
class PhotoEditorViewControllerTests: XCTestCase {
func testApplyingBlackAndWhiteFilter() {
let viewController = PhotoEditorViewController()
// 分配一個模擬照片庫以完全控制里面存儲了哪些照片
let library = PhotoLibraryMock()
library.photos = [TestPhotoFactory.photoWithColor(.red)]
viewController.library = library
// 運行我們的測試命令
viewController.selectPhoto(atIndex: 0)
viewController.apply(filter: .blackAndWhite)
viewController.savePhoto()
// 斷言結果是正確的
XCTAssertTrue(photoIsBlackAndWhite(library.photos[0]))
}
}
基于參數
最后,讓我們看一下基于參數的依賴注入。當你想輕松地使遺留代碼變得更容易測試且不必過多地改變其現有結構時,這種類型特別有用。
很多時候,我們只需要一個特定的依賴關系一次,或者我們只需要在某些條件下模擬它。我們不需要改變對象的初始化器或將屬性暴露為可變的(這并不總是一個好方式),而是可以開放某個API來接受一個依賴關系作為參數。
讓我們來看看一個NoteManager類,它是一個記事應用程序的一部分。它的工作是管理用戶所寫的所有筆記,并提供一個API用于根據查詢來搜索筆記。由于這是一個可能需要一段時間的操作(如果用戶有很多筆記的話,這是很有可能的),我們通常在一個后臺隊列中執行,像這樣:
class NoteManager {
func loadNotes(matching query: String,
completionHandler: @escaping ([Note]) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let database = self.loadDatabase()
let notes = database.filter { note in
return note.matches(query: query)
}
completionHandler(notes)
}
}
}
雖然上述方法對我們的生產代碼來說是一個很好的解決方案,但在測試中,我們通常希望盡可能地避免異步代碼和并行性,以避免片狀現象。雖然使用初始化器或基于屬性的依賴注入來指定NoteManager應始終使用的顯式隊列會很好,但這可能需要對類進行大的修改,而我們現在還不能/不愿意這樣做。
這就是基于參數的依賴性注入的作用。與其重構我們的整個類,不如直接注入要在哪個隊列上運行loadNotes操作:
class NoteManager {
func loadNotes(matching query: String,
on queue: DispatchQueue = .global(qos: .userInitiated),
completionHandler: @escaping ([Note]) -> Void) {
queue.async {
let database = self.loadDatabase()
let notes = database.filter { note in
return note.matches(query: query)
}
completionHandler(notes)
}
}
}
這使我們能夠在測試代碼中輕松地使用一個自定義隊列,我們可以在上面等待。這幾乎可以讓我們在測試中把上述API變成一個同步的API,這讓事情變得更容易和更可預測。
基于參數的依賴注入的另一個用例是當你想測試靜態API的時候。對于靜態API,我們沒有初始化器,而且我們最好也不要靜態地保持任何狀態,所以基于參數的依賴注入成為一個很好的選擇。讓我們看一個當前依賴單例的靜態 MessageSender 類:
class MessageSender {
static func send(_ message: Message, to user: User) throws {
Database.shared.insert(message)
let data: Data = try wrap(message)
let endpoint = Endpoint.sendMessage(to: user)
NetworkManager.shared.post(data, to: endpoint.url)
}
}
雖然理想的長期解決方案可能是重構MessageSender,使其成為非靜態的,并在其使用的任何地方正確注入,但為了方便測試(例如,為了重現/驗證一個錯誤),我們可以簡單地將其依賴性作為參數注入,而不是依賴單例:
class MessageSender {
static func send(_ message: Message,
to user: User,
database: Database = .shared,
networkManager: NetworkManager = .shared) throws {
database.insert(message)
let data: Data = try wrap(message)
let endpoint = Endpoint.sendMessage(to: user)
networkManager.post(data, to: endpoint.url)
}
}
我們再次使用默認參數,除去為了方便的原因,但這里更重要的是為了能夠在我們的代碼中添加測試支持,同時仍然保持100%的向后兼容性。
參考資料
[1]在Swift中使用工廠的依賴注入: https://www.swiftbysundell.com/articles/dependency-injection-using-factories-in-swift。
[2]在Swift中避免使用單例: https://www.swiftbysundell.com/articles/avoiding-singletons-in-swift。