原文地址: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 问我各种问题,给我你的评论或者反馈。
感谢阅读! 🚀