Dependency injection using factories in Swift
Dependency injection is an essential tool when it comes to making code more testable. Instead of having objects either create their own dependencies or access them as singletons, it's the idea that everything an object needs in order to do its work should be passed in from the outside. This both makes it easier to see what exact dependencies a given object has, and it also makes testing a lot simpler - since dependencies can be mocked in order to capture and verify state & values.
However, for all of its usefulness, dependency injection can also become a quite big pain point when used extensively in a project. As the number of dependencies for a given object grows, initializing it can become quite a chore. Making code testable is nice, but it's really too bad if it has to come with the cost of having initializers like this:
class UserManager {
init(dataLoader: DataLoader, database: Database, cache: Cache,
keychain: Keychain, tokenManager: TokenManager) {
...
}
}
This week, let's take a look at a dependency injection technique that lets us enable testability without forcing us to write these kind of massive initializers or complicated dependency management code.
Passing dependencies around
The main reason why we often end up in situations like the one above when using dependency injection is because we need to pass dependencies around in order to use them later. For example, let's say we're building a messaging app, and we have a view controller that displays all of the user's messages:
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)
}
}
}
As you can see above, we dependency inject a MessageLoader
into our MessageListViewController
, that it then uses to load its data. That's not too bad, since we only have a single dependency. However, our list view is likely not a dead end, which at some point will require us to implement navigation into another view controller.
Let's say we want to enable the user to navigate to a new view when tapping one of the cells in the messages list. For the new view we create a MessageViewController
, which both lets the user view the message in full, and also reply to it. To enable the reply feature we implement a MessageSender
class that we inject into our new view controller when creating it, like this:
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)
}
Here comes the problem. Since MessageViewController
needs an instance of MessageSender
, we also need to make MessageListViewController
aware of that class. One option is to simply add the sender to the list view controller's initializer as well:
class MessageListViewController: UITableViewController {
init(loader: MessageLoader, sender: MessageSender) {
...
}
}
While the above works, it starts leading us down the road to another one of those massive initializers, and makes MessageListViewController
a bit harder to use (and also quite confusing, why does the list need to be aware of the sender in the first place? 🤔).
Another possible solution (which is very common in this kind of situation) is to make MessageSender
a singleton. That way we can easily access it from anywhere, and inject it into MessageViewController
by simply using its shared instance:
let viewController = MessageViewController(
message: message,
sender: MessageSender.shared
)
However, like we took a look at in "Avoiding singletons in Swift", the singleton approach also comes with some significant downsides and can lead us into a situation of having a hard to understand architecture with unclear dependencies.
Factories to the rescue
Wouldn't it be nice if we could just skip all of the above, and enable MessageListViewController
to be completely unaware of MessageSender
, and all other dependencies that any subsequent view controllers might need?
If would both be super convenient (even more so than when introducing a singleton) - and very clean - if we could have some form of factory that we could simply ask to create a MessageViewController
for a given message, like this:
let viewController = factory.makeMessageViewController(for: message)
Like we took a look at in "Using the factory pattern to avoid shared state in Swift", one thing I really love about factories, is that they enable you to fully decouple the usage and creation of an object. This enables many objects to have a much loosely coupled relationship with their dependencies, which really helps in situations when you want to refactor or change things.
So how can we make the above happen?
We'll start by defining a protocol for our factory, which will enable us to easily create any view controller that we need in our app, without actually knowing anything about its dependencies or its initializer:
protocol ViewControllerFactory {
func makeMessageListViewController() -> MessageListViewController
func makeMessageViewController(for message: Message) -> MessageViewController
}
But we won't stop there. We'll also create additional factory protocols for creating our view controllers' dependencies as well, like this one that lets us create a MessageLoader
for our list view controller:
protocol MessageLoaderFactory {
func makeMessageLoader() -> MessageLoader
}
A single dependency
Once we have our factory protocols setup, we can go back to MessageListViewController
and refactor it to instead of taking instances of its dependencies - it now simply takes a factory:
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)
}
}
By doing the above we have now accomplished two things: First, we have reduced our dependency list into a single factory, and we have removed the need for MessageListViewController
to be aware of MessageViewController
's dependencies 🎉.
A case for a container
Now it's time to implement our factory protocols. To do that we'll start by defining a DependencyContainer
that will contain all of our app's core utility objects that are normally directly injected as dependencies. This includes things like our MessageSender
from before, but also more low-level logic classes, like any NetworkManager
we might use.
class DependencyContainer {
private lazy var messageSender = MessageSender(networkManager: networkManager)
private lazy var networkManager = NetworkManager(urlSession: .shared)
}
As you can see above, we use lazy properties in order to be able to refer to other properties of the same class when initializing our objects. This is a really convenient and nice way to setup your dependency graph, as you can utilize the compiler to help you avoid problems like circular dependencies.
Finally, we'll make our new dependency container conform to our factory protocols, which will enable us to inject it as a factory to our various view controllers and other objects:
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
It's time for the final piece of the puzzle - where do we actually store our dependency container, who should own it and where should it be setup? Here's the cool thing - since we will inject our dependency container as an implementation of the factories needed for our objects, and since those objects will hold a strong reference to their factory - there's no need for us to store the container anywhere else.
For example, if MessageListViewController
is the initial view controller of our app, we can simply create an instance of DependencyContainer
and pass it in:
let container = DependencyContainer()
let listViewController = container.makeMessageListViewController()
window.rootViewController = UINavigationController(
rootViewController: listViewController
)
No need to keep any global variables anywhere, or use optional properties in the app delegate 👍.
Conclusion
Setting up your dependency injection using factory protocols and containers can be a great way to avoid having to pass multiple dependencies around and having to create complicated initializers. While it's not a silver bullet, it can make using dependency injection easier - which will both give you a clearer picture of your objects' actual dependencies, and also make testing a lot simpler.
Since we have defined all of our factories as protocols, we can easily mock them in tests by implementing a test-specific version of any given factory protocol. I will write a lot more about mocking and how to take full advantage of dependency injection in tests in future blog posts.
What do you think? Have you used a solution like this one before, or is it something that you'll try out? Let me know, along with any other questions, comments or feedback that you might have - on Twitter @johnsundell.
Thanks for reading! 🚀