當(dāng)涉及到使代碼更加可測試時雪位,依賴注入是一個重要工具。與其讓對象創(chuàng)建自己的依賴關(guān)系或作為單例訪問它們忽你,不如讓對象在工作中需要的一切都從外部傳入蔗牡。這使我們更容易看到一個給定的對象有哪些確切的依賴關(guān)系,同時也使測試變得更加簡單——因?yàn)榭梢阅M依賴項(xiàng)以捕獲和驗(yàn)證狀態(tài)和值愚臀。
然而忆蚀,盡管它很有用,但如果在一個項(xiàng)目中廣泛使用姑裂,依賴注入也會成為一個相當(dāng)大的痛點(diǎn)馋袜。隨著一個給定對象的依賴數(shù)量的增加,初始化它可能成為一個相當(dāng)麻煩的事情炭分。讓代碼可測試是件好事桃焕,但如果要以這樣的初始化器為代價,那就太糟糕了:
class UserManager {
init(dataLoader: DataLoader, database: Database, cache: Cache,
keychain: Keychain, tokenManager: TokenManager) {
...
}
}
本周捧毛,讓我們來看看一種依賴注入技術(shù)观堂,它可以讓我們實(shí)現(xiàn)可測試性让网,而不強(qiáng)迫我們寫這種大規(guī)模的初始化器或復(fù)雜的依賴管理代碼。
傳遞依賴關(guān)系
在使用依賴注入時师痕,我們經(jīng)常會出現(xiàn)上述情況溃睹,主要原因是我們需要傳遞依賴關(guān)系,以便以后使用它們胰坟。例如因篇,假設(shè)我們正在構(gòu)建一個消息應(yīng)用程序,我們有一個視圖控制器來顯示用戶的所有消息:
class MessageListViewController: UITableViewController {
private let loader: MessageLoader
init(loader: MessageLoader) {
self.loader = loader
super.init(nibName: nil, bundle: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loader.load { [weak self] messages in
self?.reloadTableView(with: messages)
}
}
}
正如你所看到的笔横,我們將一個MessageLoader
注入到MessageListViewController
中竞滓,然后用它來加載數(shù)據(jù)。這還不算太糟吹缔,因?yàn)槲覀冎挥幸粋€依賴關(guān)系商佑。然而,我們的列表視圖很可能不是只有一層厢塘,這在某種程度上需要我們實(shí)現(xiàn)導(dǎo)航到另一個視圖控制器茶没。
假設(shè)我們想讓用戶在點(diǎn)擊消息列表中的某個單元格時,能夠?qū)Ш降揭粋€新的視圖晚碾。對于這個新的視圖抓半,我們創(chuàng)建了一個MessageViewController
,它既可以讓用戶查看消息的全文格嘁,也可以對其進(jìn)行回復(fù)笛求。為了啟用回復(fù)功能,我們實(shí)現(xiàn)了一個MessageSender
類讥蔽,在創(chuàng)建新的視圖控制器時涣易,我們將其注入到新的視圖控制器中,像這樣:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let message = messages[indexPath.row]
let viewController = MessageViewController(message: message, sender: sender)
navigationController?.pushViewController(viewController, animated: true)
}
問題來了冶伞。由于MessageViewController
需要一個MessageSender
的實(shí)例新症,我們也需要讓MessageListViewController
知道這個類。一個選擇是簡單地將發(fā)送者也添加到列表視圖控制器的初始化器中:
class MessageListViewController: UITableViewController {
init(loader: MessageLoader, sender: MessageSender) {
...
}
}
雖然上面的方法可行响禽,但它開始把我們引向另一個龐大的初始化器徒爹,并使MessageListViewController
變得更難使用(也相當(dāng)令人困惑,為什么列表首先需要知道發(fā)件人芋类???).
另一個可能的解決方案(在這種情況下很常見)是讓MessageSender
成為一個單例隆嗅。這樣我們就可以很容易地從任何地方訪問它,并通過簡單地使用它的共享實(shí)例將其注入MessageViewController
中:
let viewController = MessageViewController(
message: message,
sender: MessageSender.shared
)
然而侯繁,就像我們在 "避免在Swift中使用單例 "中看到的那樣胖喳,單例方法也有一些明顯的缺點(diǎn),可能會導(dǎo)致我們陷入一種難以理解的架構(gòu)和不明確的依賴關(guān)系的局面贮竟。
工廠模式來救援
如果我們能跳過上述所有的步驟丽焊,讓MessageListViewController
完全不知道MessageSender
较剃,以及其他任何后續(xù)視圖控制器可能需要的依賴關(guān)系,那不是更好嗎技健?
如果我們能有某種形式的工廠写穴,我們可以簡單地要求它為給定的消息創(chuàng)建一個MessageViewController
,這將是非常方便的(甚至比引入一個單例更方便)雌贱,而且非常干凈啊送,像這樣:
let viewController = factory.makeMessageViewController(for: message)
就像我們在 "使用工廠模式來避免Swift中的共享狀態(tài) "中看到的那樣,我非常喜歡工廠的一點(diǎn)是欣孤,它可以讓你完全解耦對象的使用和創(chuàng)建馋没。這使得許多對象與它們的依賴關(guān)系更加松散,這在你想要重構(gòu)或改變事物的情況下非常有幫助降传。
那么披泪,我們?nèi)绾尾拍苁股鲜銮闆r發(fā)生呢?
我們將首先為我們的工廠定義一個協(xié)議搬瑰,這將使我們能夠輕松地創(chuàng)建我們應(yīng)用程序中需要的任何視圖控制器,而不需要實(shí)際了解其依賴性或初始化器控硼。
protocol ViewControllerFactory {
func makeMessageListViewController() -> MessageListViewController
func makeMessageViewController(for message: Message) -> MessageViewController
}
但我們不會止步于此泽论。我們還將創(chuàng)建額外的工廠協(xié)議來創(chuàng)建我們的視圖控制器的依賴關(guān)系,比如這個卡乾,讓我們?yōu)槲覀兊牧斜硪晥D控制器創(chuàng)建一個MessageLoader
:
protocol MessageLoaderFactory {
func makeMessageLoader() -> MessageLoader
}
單一的依賴性
一旦我們建立了工廠協(xié)議翼悴,我們就可以回到MessageListViewController
,并重構(gòu)它幔妨,使其不再接受其依賴的實(shí)例——它現(xiàn)在只接受一個工廠鹦赎。
class MessageListViewController: UITableViewController {
// 這里我們使用協(xié)議組合來創(chuàng)建一個工廠類型,
// 其中包括這個視圖控制器需要的所有工廠協(xié)議误堡。
typealias Factory = MessageLoaderFactory & ViewControllerFactory
private let factory: Factory
// 我們現(xiàn)在可以使用注入的工廠懶加載我們的 MessageLoader古话。
private lazy var loader = factory.makeMessageLoader()
init(factory: Factory) {
self.factory = factory
super.init(nibName: nil, bundle: nil)
}
}
通過上述操作,我們現(xiàn)在已經(jīng)完成了兩件事锁施。首先陪踩,我們將我們的依賴列表縮減為一個工廠,而且我們不再需要讓MessageListViewController
知道MessageViewController
的依賴關(guān)系悉抵。
創(chuàng)建容器
現(xiàn)在是時候?qū)崿F(xiàn)我們的工廠協(xié)議了肩狂。要做到這一點(diǎn),我們首先要定義一個DependencyContainer
姥饰,它將包含我們應(yīng)用程序的所有核心實(shí)用對象傻谁,這些對象通常作為依賴關(guān)系被直接注入。這包括像之前的MessageSender
列粪,但也包括更多的低級邏輯類审磁,比如我們可能使用的NetworkManager
谈飒。
class DependencyContainer {
private lazy var messageSender = MessageSender(networkManager: networkManager)
private lazy var networkManager = NetworkManager(urlSession: .shared)
}
正如你在上面看到的,我們使用了lazy
屬性力图,以便在初始化我們的對象時能夠引用同一類別的其他屬性步绸。這是一個非常方便和漂亮的設(shè)置依賴關(guān)系的方法,因?yàn)槟憧梢岳镁幾g器來幫助你避免循環(huán)依賴等問題吃媒。
最后瓤介,我們將使我們的新依賴容器遵守我們的工廠協(xié)議,這將使我們能夠把它作為工廠注入到我們的各種視圖控制器和其他對象赘那。
extension DependencyContainer: ViewControllerFactory {
func makeMessageListViewController() -> MessageListViewController {
return MessageListViewController(factory: self)
}
func makeMessageViewController(for message: Message) -> MessageViewController {
return MessageViewController(message: message, sender: messageSender)
}
}
extension DependencyContainer: MessageLoaderFactory {
func makeMessageLoader() -> MessageLoader {
return MessageLoader(networkManager: networkManager)
}
}
分散所有權(quán)
現(xiàn)在是拼圖的最后一塊——我們究竟在哪里存儲我們的依賴容器刑桑,誰應(yīng)該擁有它,它應(yīng)該被設(shè)置在哪里募舟?最酷的是:因?yàn)槲覀儗⒆⑷胛覀兊囊蕾囆匀萜髯鳛槲覀兊膶ο笏璧墓S的實(shí)現(xiàn)祠斧,而且這些對象將持有對其工廠的強(qiáng)引用——我們沒有必要將容器存儲在其他地方。
例如拱礁,如果MessageListViewController
是我們應(yīng)用程序的初始視圖控制器琢锋,我們可以簡單地創(chuàng)建一個DependencyContainer
的實(shí)例并將其傳入:
let container = DependencyContainer()
let listViewController = container.makeMessageListViewController()
window.rootViewController = UINavigationController(
rootViewController: listViewController
)
不需要在任何地方保留任何全局變量,也不需要在應(yīng)用程序委托中使用可選屬性呢灶。
小結(jié)
使用工廠協(xié)議和容器來設(shè)置你的依賴注入是一個很好的方法吴超,可以避免傳遞多個依賴關(guān)系,以及不得不創(chuàng)建復(fù)雜的初始化器鸯乃。雖然這不是銀彈鲸阻,但它可以使依賴注入的使用更容易——這將使你更清楚地了解你的對象的實(shí)際依賴關(guān)系,同時也使測試更簡單缨睡。
由于我們已經(jīng)將所有的工廠定義為協(xié)議鸟悴,我們可以通過實(shí)現(xiàn)任何給定工廠協(xié)議的特定測試版本,在測試中輕松地模擬它們奖年。我將在未來的博文中寫更多關(guān)于模擬和如何在測試中充分利用依賴注入的內(nèi)容细诸。
你怎么看?你以前使用過像這樣的解決方案嗎陋守,或者你會嘗試一下嗎揍堰?
感謝您的閱讀 !
譯自 John Sundell 的 Dependency injection using factories in Swift