当涉及到使代码更加可测试时,依赖注入是一个重要工具。与其让对象创建自己的依赖关系或作为单例访问它们,不如让对象在工作中需要的一切都从外部传入。这使我们更容易看到一个给定的对象有哪些确切的依赖关系,同时也使测试变得更加简单——因为可以模拟依赖项以捕获和验证状态和值。
然而,尽管它很有用,但如果在一个项目中广泛使用,依赖注入也会成为一个相当大的痛点。随着一个给定对象的依赖数量的增加,初始化它可能成为一个相当麻烦的事情。让代码可测试是件好事,但如果要以这样的初始化器为代价,那就太糟糕了:
class UserManager {
init(dataLoader: DataLoader, database: Database, cache: Cache,
keychain: Keychain, tokenManager: TokenManager) {
...
}
}
本周,让我们来看看一种依赖注入技术,它可以让我们实现可测试性,而不强迫我们写这种大规模的初始化器或复杂的依赖管理代码。
传递依赖关系
在使用依赖注入时,我们经常会出现上述情况,主要原因是我们需要传递依赖关系,以便以后使用它们。例如,假设我们正在构建一个消息应用程序,我们有一个视图控制器来显示用户的所有消息:
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
中,然后用它来加载数据。这还不算太糟,因为我们只有一个依赖关系。然而,我们的列表视图很可能不是只有一层,这在某种程度上需要我们实现导航到另一个视图控制器。
假设我们想让用户在点击消息列表中的某个单元格时,能够导航到一个新的视图。对于这个新的视图,我们创建了一个MessageViewController
,它既可以让用户查看消息的全文,也可以对其进行回复。为了启用回复功能,我们实现了一个MessageSender
类,在创建新的视图控制器时,我们将其注入到新的视图控制器中,像这样:
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
,以及其他任何后续视图控制器可能需要的依赖关系,那不是更好吗?
如果我们能有某种形式的工厂,我们可以简单地要求它为给定的消息创建一个MessageViewController
,这将是非常方便的(甚至比引入一个单例更方便),而且非常干净,像这样:
let viewController = factory.makeMessageViewController(for: message)
就像我们在 "使用工厂模式来避免Swift中的共享状态 "中看到的那样,我非常喜欢工厂的一点是,它可以让你完全解耦对象的使用和创建。这使得许多对象与它们的依赖关系更加松散,这在你想要重构或改变事物的情况下非常有帮助。
那么,我们如何才能使上述情况发生呢?
我们将首先为我们的工厂定义一个协议,这将使我们能够轻松地创建我们应用程序中需要的任何视图控制器,而不需要实际了解其依赖性或初始化器。
protocol ViewControllerFactory {
func makeMessageListViewController() -> MessageListViewController
func makeMessageViewController(for message: Message) -> MessageViewController
}
但我们不会止步于此。我们还将创建额外的工厂协议来创建我们的视图控制器的依赖关系,比如这个,让我们为我们的列表视图控制器创建一个MessageLoader
:
protocol MessageLoaderFactory {
func makeMessageLoader() -> MessageLoader
}
单一的依赖性
一旦我们建立了工厂协议,我们就可以回到MessageListViewController
,并重构它,使其不再接受其依赖的实例——它现在只接受一个工厂。
class MessageListViewController: UITableViewController {
// 这里我们使用协议组合来创建一个工厂类型,
// 其中包括这个视图控制器需要的所有工厂协议。
typealias Factory = MessageLoaderFactory & ViewControllerFactory
private let factory: Factory
// 我们现在可以使用注入的工厂懒加载我们的 MessageLoader。
private lazy var loader = factory.makeMessageLoader()
init(factory: Factory) {
self.factory = factory
super.init(nibName: nil, bundle: nil)
}
}
通过上述操作,我们现在已经完成了两件事。首先,我们将我们的依赖列表缩减为一个工厂,而且我们不再需要让MessageListViewController
知道MessageViewController
的依赖关系。
创建容器
现在是时候实现我们的工厂协议了。要做到这一点,我们首先要定义一个DependencyContainer
,它将包含我们应用程序的所有核心实用对象,这些对象通常作为依赖关系被直接注入。这包括像之前的MessageSender
,但也包括更多的低级逻辑类,比如我们可能使用的NetworkManager
。
class DependencyContainer {
private lazy var messageSender = MessageSender(networkManager: networkManager)
private lazy var networkManager = NetworkManager(urlSession: .shared)
}
正如你在上面看到的,我们使用了lazy
属性,以便在初始化我们的对象时能够引用同一类别的其他属性。这是一个非常方便和漂亮的设置依赖关系的方法,因为你可以利用编译器来帮助你避免循环依赖等问题。
最后,我们将使我们的新依赖容器遵守我们的工厂协议,这将使我们能够把它作为工厂注入到我们的各种视图控制器和其他对象。
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
是我们应用程序的初始视图控制器,我们可以简单地创建一个DependencyContainer
的实例并将其传入:
let container = DependencyContainer()
let listViewController = container.makeMessageListViewController()
window.rootViewController = UINavigationController(
rootViewController: listViewController
)
不需要在任何地方保留任何全局变量,也不需要在应用程序委托中使用可选属性。
小结
使用工厂协议和容器来设置你的依赖注入是一个很好的方法,可以避免传递多个依赖关系,以及不得不创建复杂的初始化器。虽然这不是银弹,但它可以使依赖注入的使用更容易——这将使你更清楚地了解你的对象的实际依赖关系,同时也使测试更简单。
由于我们已经将所有的工厂定义为协议,我们可以通过实现任何给定工厂协议的特定测试版本,在测试中轻松地模拟它们。我将在未来的博文中写更多关于模拟和如何在测试中充分利用依赖注入的内容。
你怎么看?你以前使用过像这样的解决方案吗,或者你会尝试一下吗?
感谢您的阅读 !
译自 John Sundell 的 Dependency injection using factories in Swift