在之前的文章中抹恳,我們看了一些使用依賴注入的不同方法犬辰,以實(shí)現(xiàn)Swift應(yīng)用中更多的解耦和可測(cè)試架構(gòu)钞翔。例如黑忱, "在Swift中使用工廠的依賴注入"中把依賴注入和工廠模式結(jié)合起來,以及"在Swift中避免使用單利" 中利用依賴注入取代單利袄膏。
到目前為止,我的大部分文章和例子都使用了基于初始化器的依賴注入掺冠。然而沉馆,就像大多數(shù)編程技術(shù)一樣,依賴注入有多種“風(fēng)味(Flavors)”德崭,每一種都有自己的優(yōu)點(diǎn)和缺點(diǎn)斥黑。本周,讓我們來看看三種不同方式的依賴注入眉厨,以及它們?nèi)绾卧赟wift中使用锌奴。
基于初始化器
讓我們先快速回顧一下最常見的依賴注入方式——基于初始化器的依賴注入,即對(duì)象在被初始化時(shí)應(yīng)該被賦予它所需要的依賴關(guān)系憾股。這種方式的最大好處是鹿蜀,它保證我們的對(duì)象擁有它們所需要的一切箕慧,以便立即開展工作。
假設(shè)我們正在構(gòu)建一個(gè)從磁盤上加載文件的FileLoader
茴恰。為了做到這一點(diǎn)颠焦,它使用了兩個(gè)依賴項(xiàng)——一個(gè)是系統(tǒng)提供的FileManager
的實(shí)例,另一個(gè)是Cache
往枣。使用基于初始化器的依賴注入伐庭,可以這樣實(shí)現(xiàn):
class FileLoader {
private let fileManager: FileManager
private let cache: Cache
init(fileManager: FileManager = .default,
cache: Cache = .init()) {
self.fileManager = fileManager
self.cache = cache
}
}
注意上面是如何使用默認(rèn)參數(shù)的,以避免在使用單例或新實(shí)例時(shí)總是創(chuàng)建依賴關(guān)系分冈。這使我們能夠在生產(chǎn)代碼中使用FileLoader()
簡(jiǎn)單地創(chuàng)建一個(gè)文件加載器圾另,同時(shí)仍然能夠通過在測(cè)試代碼中注入模擬數(shù)據(jù)或顯式實(shí)例進(jìn)行測(cè)試。
基于屬性
雖然基于初始化器的依賴注入通常很適合你自己的自定義類雕沉,但有時(shí)當(dāng)你必須從系統(tǒng)類繼承時(shí)集乔,它就有點(diǎn)難用了。一個(gè)例子是在構(gòu)建視圖控制器時(shí)蘑秽,特別是當(dāng)你使用 XIBs 或 Storyboards 來定義它們時(shí)饺著,因?yàn)檫@樣你就無法再控制你的類的初始化器了。
對(duì)于這些類型的情況肠牲,基于屬性的依賴注入可以是一個(gè)很好的選擇幼衰。與其在對(duì)象的初始化器中注入對(duì)象的依賴關(guān)系,不如在之后簡(jiǎn)單地將其分配缀雳。這種依賴注入的方式也可以幫助你減少模板文件渡嚣,特別是當(dāng)有一個(gè)好的默認(rèn)值不一定需要注入的時(shí)候。
讓我們來看看另一個(gè)例子——在這個(gè)例子中肥印,我們要建立一個(gè)PhotoEditorViewController
识椰,讓用戶編輯他們庫中的一張照片。為了發(fā)揮作用深碱,這個(gè)視圖控制器需要一個(gè)系統(tǒng)提供的PHPhotoLibrary
類的實(shí)例(它是一個(gè)單例)腹鹉,以及一個(gè)我們自己的PhotoEditorEngine
類的實(shí)例。為了在沒有自定義初始化器的情況下實(shí)現(xiàn)依賴性注入敷硅,我們可以創(chuàng)建兩個(gè)都有默認(rèn)值的可變屬性功咒,就像這樣:
class PhotoEditorViewController: UIViewController {
var library: PhotoLibrary = PHPhotoLibrary.shared()
var engine = PhotoEditorEngine()
}
請(qǐng)注意 "通過 3 個(gè)簡(jiǎn)單的步驟測(cè)試使用了系統(tǒng)單例的 Swift 代碼"中的技術(shù)是如何通過使用協(xié)議來為系統(tǒng)照片庫類提供一個(gè)更抽象的PhotoLibrary
接口。這將使測(cè)試和數(shù)據(jù)模擬變得更加容易!
上述做法的好處是绞蹦,我們?nèi)匀豢梢院苋菀椎卦跍y(cè)試中注入模擬數(shù)據(jù)力奋,只需重新分配視圖控制器的屬性:
class PhotoEditorViewControllerTests: XCTestCase {
func testApplyingBlackAndWhiteFilter() {
let viewController = PhotoEditorViewController()
// 分配一個(gè)模擬照片庫以完全控制里面存儲(chǔ)了哪些照片
let library = PhotoLibraryMock()
library.photos = [TestPhotoFactory.photoWithColor(.red)]
viewController.library = library
// 運(yùn)行我們的測(cè)試命令
viewController.selectPhoto(atIndex: 0)
viewController.apply(filter: .blackAndWhite)
viewController.savePhoto()
// 斷言結(jié)果是正確的
XCTAssertTrue(photoIsBlackAndWhite(library.photos[0]))
}
}
基于參數(shù)
最后,讓我們看一下基于參數(shù)的依賴注入幽七。當(dāng)你想輕松地使遺留代碼變得更容易測(cè)試且不必過多地改變其現(xiàn)有結(jié)構(gòu)時(shí)景殷,這種類型特別有用。
很多時(shí)候猿挚,我們只需要一個(gè)特定的依賴關(guān)系一次咐旧,或者我們只需要在某些條件下模擬它。我們不需要改變對(duì)象的初始化器或?qū)傩员┞稙榭勺兊模ㄟ@并不總是一個(gè)好方式)亭饵,而是可以開放某個(gè)API來接受一個(gè)依賴關(guān)系作為參數(shù)。
讓我們來看看一個(gè)NoteManager
類辜羊,它是一個(gè)記事應(yīng)用程序的一部分。它的工作是管理用戶所寫的所有筆記碱妆,并提供一個(gè)API用于根據(jù)查詢來搜索筆記。由于這是一個(gè)可能需要一段時(shí)間的操作(如果用戶有很多筆記的話昔驱,這是很有可能的)骤肛,我們通常在一個(gè)后臺(tái)隊(duì)列中執(zhí)行纳本,像這樣:
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)
}
}
}
雖然上述方法對(duì)我們的生產(chǎn)代碼來說是一個(gè)很好的解決方案,但在測(cè)試中腋颠,我們通常希望盡可能地避免異步代碼和并行性繁成,以避免片狀現(xiàn)象。雖然使用初始化器或基于屬性的依賴注入來指定NoteManager
應(yīng)始終使用的顯式隊(duì)列會(huì)很好淑玫,但這可能需要對(duì)類進(jìn)行大的修改巾腕,而我們現(xiàn)在還不能/不愿意這樣做。
這就是基于參數(shù)的依賴性注入的作用絮蒿。與其重構(gòu)我們的整個(gè)類尊搬,不如直接注入要在哪個(gè)隊(duì)列上運(yùn)行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)
}
}
}
這使我們能夠在測(cè)試代碼中輕松地使用一個(gè)自定義隊(duì)列,我們可以在上面等待土涝。這幾乎可以讓我們?cè)跍y(cè)試中把上述API變成一個(gè)同步的API佛寿,這讓事情變得更容易和更可預(yù)測(cè)。
基于參數(shù)的依賴注入的另一個(gè)用例是當(dāng)你想測(cè)試靜態(tài)API的時(shí)候但壮。對(duì)于靜態(tài)API冀泻,我們沒有初始化器,而且我們最好也不要靜態(tài)地保持任何狀態(tài)茵肃,所以基于參數(shù)的依賴注入成為一個(gè)很好的選擇。讓我們看一個(gè)當(dāng)前依賴單例的靜態(tài)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)
}
}
雖然理想的長(zhǎng)期解決方案可能是重構(gòu)MessageSender
袭祟,使其成為非靜態(tài)的验残,并在其使用的任何地方正確注入,但為了方便測(cè)試(例如巾乳,為了重現(xiàn)/驗(yàn)證一個(gè)錯(cuò)誤)您没,我們可以簡(jiǎn)單地將其依賴性作為參數(shù)注入鸟召,而不是依賴單例:
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)
}
}
我們?cè)俅问褂媚J(rèn)參數(shù),除去為了方便的原因氨鹏,但這里更重要的是為了能夠在我們的代碼中添加測(cè)試支持欧募,同時(shí)仍然保持100%的向后兼容性。
譯自 John Sundell 的 Different flavors of dependency injection in Swift