依赖注入是提高代码可测试性的必要工具。与其让对象创建它们自己的依赖项或作为单例访问它们,它的思想是,一个对象为了完成它的工作所需要的一切都应该从外部传入。这既使它更容易看到一个给定对象有哪些确切的依赖项,也使测试变得简单得多——因为依赖项可以通过模拟来捕获和验证状态和值。
然而,尽管依赖注入很有用,但当在项目中广泛使用时,它也可能成为一个相当大的痛点。 随着给定对象的依赖项数量的增长,初始化它可能会变得相当繁琐。让代码变得可测试是件好事,但如果它需要像这样的初始化器成本,那就太糟糕了:
class UserManager {
init(dataLoader: DataLoader, database: Database, cache: Cache,
keychain: Keychain, tokenManager: TokenManager) {
...
}
}
本周,让我们来看看一种依赖注入技术,它使我们能够实现可测试性,而不必强迫我们编写此类大规模初始化程序或复杂的依赖管理代码。
Passing dependencies around
当使用依赖注入时,我们经常会出现如上所述的情况,主要原因是我们需要传递依赖以便以后使用它们。例如,假设我们正在构建一个消息应用程序,我们有一个视图控制器来显示所有用户的消息:
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,然后它用它来加载它的数据。这并不太糟糕,因为我们只有一个依赖项。然而,我们的列表视图可能不是一个死胡同,这在某种程度上将要求我们实现导航到另一个视图控制器。
假设我们想让用户在点击messages列表中的一个单元格时能够导航到一个新视图。对于新视图,我们创建了一个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知道这个类。一种方法是把sender也添加到列表视图控制器的初始化器中:
class MessageListViewController: UITableViewController {
init(loader: MessageLoader, sender: MessageSender) {
...
}
}
虽然上面的工作,它开始把我们引向另一个大规模的初始化器,并使MessageListViewController有点难以使用(也相当令人困惑,为什么列表需要知道发送者在第一个地方?🤔)。
另一个可能的解决方案(在这种情况下非常常见)是使MessageSender成为一个单例。这样我们就可以轻松地从任何地方访问它,并通过使用它的共享实例将它注入到MessageViewController中:
let viewController = MessageViewController(
message: message,
sender: MessageSender.shared
)
然而,就像我们在“Swift中避免单例”中看到的那样,单例方法也有一些明显的缺点,可能会导致我们陷入一种难以理解的架构和不清楚的依赖关系的情况。
Factories to the rescue
如果我们可以跳过上面的所有这些,并使MessageListViewController完全不知道MessageSender,以及所有其他任何后续视图控制器可能需要的依赖,这不是很好吗?
如果两者都超级方便(甚至比引入单例时更方便)——而且非常干净——如果我们可以有某种形式的工厂,我们可以简单地要求为给定的消息创建一个MessageViewController,像这样:
let viewController = factory.makeMessageViewController(for: message)
就像我们在“使用工厂模式避免Swift中的共享状态”中看到的那样,我真正喜欢工厂的一点是,它们使您能够完全解耦对象的使用和创建。这使得许多对象与它们的依赖项之间具有非常松散的耦合关系,这在您想要重构或更改内容的情况下确实很有帮助。
So how can we make the above happen?
我们首先为我们的工厂定义一个协议,这将使我们能够轻松地创建任何我们在应用中需要的视图控制器,而不需要真正了解它的依赖项或初始化器:
protocol ViewControllerFactory {
func makeMessageListViewController() -> MessageListViewController
func makeMessageViewController(for message: Message) -> MessageViewController
}
但我们不会就此止步。我们还会创建额外的工厂协议来创建视图控制器的依赖项,就像这个让我们为列表视图控制器创建一个MessageLoader:
protocol MessageLoaderFactory {
func makeMessageLoader() -> MessageLoader
}
A single dependency
一旦我们建立了我们的工厂协议,我们可以回到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的依赖🎉
A case for a container
现在是时候实施我们的工厂协议了。要做到这一点,我们首先定义一个DependencyContainer,它将包含我们应用程序的所有核心实用对象,这些对象通常直接作为依赖项注入。这包括以前的MessageSender,也包括更低级的逻辑类,如我们可能使用的任何NetworkManager。
class DependencyContainer {
private lazy var messageSender = MessageSender(networkManager: networkManager)
private lazy var networkManager = NetworkManager(urlSession: .shared)
}
正如您在上面看到的,我们使用延迟属性是为了在初始化对象时能够引用同一个类的其他属性。这是设置依赖关系图的一种非常方便和好的方法,因为您可以利用编译器来帮助您避免诸如循环依赖关系之类的问题。
最后,我们将使我们的新依赖容器符合我们的工厂协议,这将使我们能够将它作为一个工厂注入到我们的各种视图控制器和其他对象:
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)
}
}
Distributed ownership
现在是解决这个难题的最后一步了——我们实际上在哪里存储我们的依赖容器,谁应该拥有它,它应该在哪里设置? 这里有一件很酷的事情——因为我们将把依赖容器注入为我们的对象所需的工厂的实现,而且因为这些对象将持有对其工厂的强引用——我们没有必要将容器存储在其他任何地方。
例如,如果MessageListViewController是我们应用程序的初始视图控制器,我们可以简单地创建一个DependencyContainer实例并将它传入:
let container = DependencyContainer()
let listViewController = container.makeMessageListViewController()
window.rootViewController = UINavigationController(
rootViewController: listViewController
)
不需要在任何地方保留任何全局变量,也不需要在应用委托中使用可选属性👍。
Conclusion
使用工厂协议和容器来设置依赖项注入是避免传递多个依赖项和创建复杂初始化器的好方法。虽然它不是银弹,但它可以让依赖注入的使用变得更容易——这将让你更清楚地了解对象的实际依赖关系,同时也使测试变得更简单。
由于我们已经将所有工厂定义为协议,我们可以通过实现任何给定工厂协议的特定于测试的版本,轻松地在测试中模拟它们。我将在以后的博文中写更多关于mock以及如何在测试中充分利用依赖注入的内容。