Single Instance + Dependency Injection + Service Locator Pattern + iOS + Swift
Managing dependencies of a large application will always be a challenging task. This is also true for iOS apps, where there are countless different patterns and architectures such as MVC, VIPER, MVVM, VIP, MVP etc..
Even if you don’t use any mentioned architectures and you have a homebrewed solution or mixture of few, there will be a need for a common way to manage the dependencies throughout the whole app.
Before reading further, I would highly recommend my previous blog about, dependency injection here. This blog will be about a slightly different variation of the previous one.
Let’s get started, first of all, the aim of this blog is to achieve a common way of managing dependencies, creating single instance dependencies, testability, you might also realize more opportunities related to composability and even maybe benefits to modularisation, depending on your project. I will try to describe each class that is needed for the dependency provider.
DependencyProvider class
It owns the instance of the ServiceProvider and UtilityProvider, also conforms to the DependencyProviding Protocol, It has the following methods to register services and other utility classes.
DependencyProviding Protocol
DependencyProviding protocol conforms to the ServiceProviding and UtilityProviding protocols.
ServiceProvider Class
ServiceProvider has a method called register where it returns the existing instance of the service or just creates a new instance and adds the instance to a dictionary of instances.
In this implementation below, we guarantee that the same service is only added once.
Service Protocol
We also need to have a Service protocol with an initializer signature, in order to unify the initialization of every Service conforming to this protocol so that we can use our generic register method in the ServiceProvider.
ServiceProvidingProtocol Class
This protocol has the method signature for registering service.
UtilityProvider class
UtilityProvider is similar to ServiceProvider in terms of managing instances, the only difference is that rather than managing a common Service type, it has mixed types like AnalyticsTracker, HttpClient or anything related to utilities, helpers can be managed in the UtilityProvider class.
UtilityProvividing Protocol
This protocol has the method signatures for registering different utility classes.
So far this is what we need in terms of abstraction for our dependency provider, feel free to have different providers such as CoordinatorProvider, RepositoryProvider, RouterProvider for your dependencies. It is your choice, the reason why you might want to include other components might be to introduce the common way to initialize other dependencies as well.
Few things to be aware, about the dependency provider;
- The current implementation of the dependency provider only creates a single instance service meaning, the same instance will be shared across all layers. This is the basis of the Service Locator pattern. However one thing to be aware, as soon as you have a single instance service, it is a good idea not to have any public properties on these single instance classes shared or mutable.
- You can also create methods in the ServiceProvider where you might want to create a new instance of the service rather than sharing the same instance. I will leave this choice to you.
- My main motivation for having single instance services is because my services do not hold any states, so there is no need for services being created multiple times since they access the repository for reading and writing data.
Assuming we have a simple MVVM example, where first DependencyProvider class is initialized in the AppDelegate as a lazy property, then it is injected in the UserViewModel.
In the UserViewModel, we can see, how we are able to create other dependencies by registering the services.
In the UserService we register instances of AnalyticsTracker and HttpClient
Following our example, we have successfully injected the dependency provider instance between the layers
AppDelegate (Entry Point)
[DependencyProvider Instance] ▼
ViewModel (Layer 1)
[DependencyProvider Instance] ▼
Service (Layer 2)
In your case, you might have more layers such as Coordinator, Router, ViewController, ViewModel, Service, Repository/DAO. Following the same pattern, it will be possible to inject the dependency provider between any number of layers.
Finally, we have achieved a common way of injecting dependencies across the whole architecture of the app.
What about testability? Does this make my life easier? The answer is yes, and the maintainability cost of this is quite low, in terms of things to do when you add new services or classes since the pattern is the same.
Looking at MockDependencyProvider, we can see that rather than real implementation this time we provide the mock services or other mock classes, the only thing you will have to do is to map the real classes to mock classes so that mocks can be automatically added your tests
In the UserViewModelTests, mock dependency provider is injected in the UserViewModel, because we already mapped the real classes to the mock classes, they are added in the mock dependency provider dictionary, you can easily access and set the mock properties for different test scenarios, like below.
Do not worry, if it does not make sense at first. There is a github example project.
Thanks for reading. I have tried to explain yet another way of managing dependencies. Feel free to modify, change names of the classes, or the abstraction or even take it further.