原文地址:Dependency injection using factories in Swift
如果想要讓代碼更加可測試,依賴注入是不可缺少的手段。依賴注入的思想是,一個對象運作所需要的任何東西都要從外部傳入,而不是讓對象自身去生成自己的依賴對象,或者從單例獲取它們。這能讓我們更加容易看到某個給定的對象到底需要哪些依賴,并且使得測試更簡單 - 因為依賴是可以通過mock來捕獲并驗證其狀態以及值。
然而盡管依賴注入很有用,但當它在一個項目里大量使用時,也會變成一個很大的痛點。隨著一個對象的依賴數量越來越多,初始化該對象也會變得非常繁瑣。讓代碼變得可測試是好的,但如果其代價是讓代碼的初始化函數變成下面這樣,就再糟糕不過了。
class UserManager {
init(dataLoader: DataLoader, database: Database, cache: Cache,
keychain: Keychain, tokenManager: TokenManager) {
...
}
}
本周就讓我們來看一看一種依賴注入技巧,它讓我們不需要寫這種巨長的初始化函數或者寫很復雜的依賴管理代碼,也能保證可測試性。
傳入依賴關系
當使用依賴注入時,我們經常遇到上述情況的主要原因是因為我們需要傳遞依賴關系以便于稍后使用它們。比如,假設我們開發一個消息 app,并且我們有一個 view controller 來顯示所有用戶的消息:
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 中,然后用它來加載數據。這還算可以,因為我們只有一個依賴。但是,我們的 list view 并沒有完,在某些時候,我們需要實現導航到另一個 view controller。
假設我們希望用戶在點擊消息列表中的一個 cell 時,能夠導航到新的視圖。對于新視圖,我們創建了一個 MessageViewController,它可以讓用戶查看完整的消息并回復它。 為了啟用回復功能,我們實現了一個 MessageSender 類,在創建它時我們將其注入到新的 view controller 中,如下所示:
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 實例,所以我們也需要讓 MessageListViewController 知道這個類。一種選擇是簡單地將發送者添加到列表視圖控制器的初始化函數中:
class MessageListViewController: UITableViewController {
init(loader: MessageLoader, sender: MessageSender) {
...
}
}
雖然上面的代碼能工作,但是它開始帶領我們走向重量級初始化函數的道路上了,而且使得 MessageListViewController 有點難用(而且很讓人迷惑,為什么列表一開始就需要知道發送者???)。
另一種可能的解決方案(在這種情況下非常常見)是使 MessageSender 成為一個單例。 這樣我們可以從任何地方輕松地訪問它,并通過簡單地使用它的共享實例將其注入到 MessageViewController 中:
let viewController = MessageViewController(
message: message,
sender: MessageSender.shared
)
然而,就像我們在《Swift中避免使用單例》中所看到的那樣,單例方法也帶來了一些重大的缺點,并且可能導致我們陷入一個難以理解的,依賴性不清的架構中。
使用工廠模式來拯救
如果我們可以跳過上述所有內容,并且使 MessageListViewController 完全不知道 MessageSender 以及任何后續 view controller 可能需要的所有其他依賴關系,豈不妙哉?
假設我們可以使用某種形式的“工廠”可以為指定的消息生成一個 MessageViewController,比如:
let viewController = factory.makeMessageViewController(for: message)
這樣既非常方便(甚至比引入單例更加方便),也非常簡潔。
就像我們在《在Swift中使用工廠模式避免共享狀態》提到的一樣,我特別喜歡工廠的一點是,它們能夠完全分離對象的使用和創建。 這使得許多對象與它們的依賴關系有著非常松散的耦合關系,這在需要重構或修改的情況下非常有用。
如何實現上述例子
首先,我們為工廠定義一個 protocol,它使我們能夠在我們的 app 中輕松地創建任何 view controller,而無需真正知道其依賴項或其初始化函數。
protocol ViewControllerFactory {
func makeMessageListViewController() -> MessageListViewController
func makeMessageViewController(for message: Message) -> MessageViewController
}
但我們不會就此打住。我們還要創建更多的工廠協議來創建 view controller 的依賴關系。 如同下面的這個 protocol,它能為我們的 list view controller 創建 Message Loader:
protocol MessageLoaderFactory {
func makeMessageLoader() -> MessageLoader
}
單一依賴項
一旦我們設置好了工廠協議,我們可以回到 MessageListViewController 并重構它。現在它只需要一個工廠,而不是一系列依賴項的實例:
class MessageListViewController: UITableViewController {
// Here we use protocol composition to create a Factory type that includes
// all the factory protocols that this view controller needs.
typealias Factory = MessageLoaderFactory & ViewControllerFactory
private let factory: Factory
// We can now lazily create our MessageLoader using the injected factory.
private lazy var loader = factory.makeMessageLoader()
init(factory: Factory) {
self.factory = factory
super.init(nibName: nil, bundle: nil)
}
}
通過上述操作,我們現在完成了兩件事情:首先,我們將依賴列表減少為單個工廠,并且我們已經不再需要 MessageListViewController 知道 MessageViewController的依賴關系了。
容器
現在我們來實現工廠協議。我們首先定義一個 DependencyContainer,它將包含所有通常直接作為依賴項注入的核心 utility 對象。這包括之前 MessageSender 這樣的東西,也包括更底層的邏輯類,比如任何可能用到的 NetworkManager。
如上所示,我們使用了 lazy property 使得在初始化對象時,我們可以引用該類里面的其他 property。這是創建依賴關系圖既非常方便又漂亮的方法,因為你可以使用編譯器來幫助你避免循環依賴等問題。
最后,我們使新建的依賴容器符合我們的工廠協議,這讓我們能夠把它作為工廠注入到各種 view controller 和其他對象中去:
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)
}
}
分散的所有權
現在是拼圖的最后一部分了 - 我們在哪里存儲依賴容器?誰應該擁有它?它應該在哪里配置?這便是這個架構的厲害之處:由于我們將依賴容器作為對象所需工廠的實現進行注入,并且這些對象會使用強引用來持有工廠對象,我們沒必要再別的地方保存容器。
例如,如果 MessageListViewController 是我們 app 的初始 view controller,我們可以簡單地創建一個 DependencyContaine r實例,并將其傳入:
let container = DependencyContainer()
let listViewController = container.makeMessageListViewController()
window.rootViewController = UINavigationController(
rootViewController: listViewController
)
并不需要在其他任何地方保留全局變量,或者放在 app delegate 中作為 optional properties??。
總結
使用工廠協議和容器來配置依賴注入是避免傳遞多個依賴、創建復雜初始化函數的好方法。雖然它不是一顆銀彈,但它可以讓使用依賴注入更加容易。著讓你能更清楚地了解對象的實際依賴關系,也讓測試更簡單。
由于我們已經將所有的工廠定義為協議,我們可以通過對任一協議方法實現一套測試專用版本,來輕松地模擬它們。 我會在之后的博文中寫更多有關 mock 的東西,以及如何在測試中充分利用依賴注入。
對此你有什么看法?你之前是否使用過類似的方案?或者說你會嘗試一下這套方案嗎?請告訴我。你也通過評論區或者推特 @johnsundell 問我各種問題,給我你的評論或者反饋。
感謝閱讀! ??