This blog will cover the following things:
- What is Inversion Of Control
- What is Dependency Injection
- What is DI Container
- Basic Usage of Swinject
- Why We Need Swinject
- Object Scopes
- Modularize Dependency Registration Using Assembler
Inversion Of Control (IOC)
In object-oriented programming it’s all about to remove the dependencies from your code.
In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. Dependency injection is one way to achieve the Inversion Of Control.
The idea behind this is to have a separate object create the required dependency, and pass it to the client.
- To achieve Dependency Inversion Principle
Dependency Injection Container is an object that knows how to instantiate and configure objects. DI Container is a design pattern to implement Dependency injection. One benefit of using it to resolve Complex dependency
We need to test
loadData method of
NetworkManager class. This method uses
UrlSession to fetch data from the server and decode the response in the model. We can’t test this method until we have an internet connection which is not a very good practice. What if we somehow mock the server by injecting mock
Urlsession which return mock data.
As shown in Figure 2 we created
MockUrlSessionDataTask classes that act as a mock server which will return what we told. If you want to deep dive into the network testing strategies you can see this blog.
Come to the main point. We created
urlSession property and injected it through its initializer. Now
NetworkManager is not responsible for creating
UrlSession since we inject it through its initializer what we achieved.
- We removed one unnecessary responsibility of the class to create it’s dependent/external objects. (Goal is to achieve the Single Responsibility Principle).
- We implemented a Dependency Injection by using its Initializer Injection pattern. As said above “dependency injection is a technique whereby one object supplies the dependencies of another object”. Now if any object wants to use this class it needs to provide its dependency.
As shown in Figure 3 we achieved testability using
dependency injection but where is Swinject ?? 😖
Why We Need Swinject
DI Container is a design pattern to implement Dependency injection. Most of the time, you don’t need a Dependency Injection Container to benefit from Dependency Injection. But when you need to manage a lot of different objects with a lot of dependencies, a Dependency Injection Container can be really helpful. Swinject is the implementation of DI Container in Swift.
As shown in Figure 4 we have a client A and B both need
DataFetcher object. We are applying Dependency Injection by passing
DataFetcher dependencies through its initializer. What can we observe by looking into this complex dependencies:
- Duplicate code in Client A and Client B.
- Can we have a central location, whose responsibility is to provide object by creating all its dependencies?
This is where DI container will be helpful. DI container manages the type dependencies of your system. First, you register the types that should be resolved, with their dependencies. Then you use the DI container to get instances of those types whose dependencies are then automatically resolved by the DI container. In Swinject, the Container class represents the DI container.
Before we implement the above code using
Swinject we first need to have a good understanding of how Swinject works, it’s syntax and basic usage. You can also see this link.
- Service → A protocol defining an interface for a dependent type.
- Component → An actual type of implementing a service.
- Factory → A function or closure instantiating a component/dependent object (In Our case logic of How to create DataFetcher with its dependencies).
- Container → A collection of component instances. (DI Container in which our dependent object register and resolve).
As shown in Figure 5 we performed a number of tasks to resolve our dependency in a central location:
- First, we get the container instance using its constructor, Remember Container class is not a singleton class. So every time you call its constructor you will get the new instance.
- We register
DataFetcherin a container and how to resolve its dependencies/or it’s creation logic we define in its factory which is a closure. Note:
DataFetcher.selfacts as a
- Now if a client wants
DataFetcherinstance it calls the resolve method of a container and DI container first check if this type of Service already registered if yes it execute factory closure and after resolving dependencies it returns the fresh
DataFetcherobject with all the dependencies populated as shown in the console in Figure 5. Note: container instance should be the same when registering and when resolving.
As shown in Figure 6 we register using
Fetcher.self as a service Protocol (abstract type as compared to the previous example) and resolve it using the container. This is a simple example:
As shown in Figure 7 we have two classes that implement the same
Fetcher protocol and the question is how to register the same Service Type Protocol and this is where Named Registration in a DI Container comes to play. As you can see we register two Fetcher with name parameter in which one return
FetchFromDatabase instance and other
FetchFromServer instance. When resolving we do the same thing:
As shown in Figure 8 Now Our Object that needs to be resolved by DI container needs to have some dynamic configuration This is where Registration with Arguments in a DI Container comes into play. The factory closure passed to the register method can take arguments that are passed when the service is resolved. When you register the service, the arguments can be specified after the Resolver parameter. Note: you can pass up to 9 arguments in Swinject.
Now we understand the basics and syntax of Swinject. This is how Figure 4 would look like when using Swinject
As shown in Figure 9 we created
sharedContainer a property in
Container class using extension which will return container singleton object until all registration have been done. We told the container how to create an instance of
DataFetcher in its factory closure of register method.
As shown in Figure 10 we call the resolve method on client class and telling it to create an instance of DataFetcher now container will execute its factory method and after it resolved its dependency it will return DataFetcher instance. As said earlier benefits of DI container is to “resolve Complex dependency” and also we achieved a central location whose responsibility is to create an instance of an object after resolving all its dependencies.
Now every time you call resolve on
DataFetcher.self it will always return a new instance of DataFetcher object as shown in Figure 11. What if we want the same instance this is where Object Scopes comes into play:
Object scope is a configuration option to determine how an instance provided by a DI container is shared in the system. There are four scopes supported by Swinject
Graph (the default scope) → It always creates a new instance when you call resolve but while resolving graph it shared the instances.
As shown in Figure 12 there is a
class A depends on B and C and
C class depends on B
=========== Graph A = A depends on --> (B and (C -> B)) ==============
When client call resolve on
A these are the action performed by the resolver:
ADependency which is
B, first, check
Bdependency and didn’t find any then created
Binstance(0x0000600002aa69d0) and put in shared container since graph is not resolved yet.
- Now resolving C, first check it’s dependency which is B then look into the shared container and find B with address(0x0000600002aa69d0) and assign this
Cproperty and finally created
Aresolved all its dependencies it returns A object and deleted shared container since the graph is resolved.
As shown in Figure 12 B address is the same on A and C classes.
Transient → which always returns a new object. (an instance provided by a container is not shared).
As shown in Figure 13 by making B scope transient we are saying that doesn't share it’s instance as shown in Console B address is different on both A and C instances:
Container → This scope is also known as Singleton. which means once the object is resolved it will share that instance for every resolve method until the application terminates as shown in Figure 14:
Custom Scopes → Instances in
.custom the scope will be shared in the same way as in
.container scope but can be discarded as needed.
As shown in Figure 15 we performed a number of tasks to implement Custom Scopes:
- We extend
ObjectScopeand add the custom static property and made a scope of this storage type
PermanentStorage(Means Persists stored instance until it is explicitly discarded ).
- We register
UserSessionwith custom object scope.
loginInUserthe method we get the instance of
UserSessionand as shown in Figure 15 set its token. Now every time we resolve
UserSessionwe get the same instance as shown in the console.
logOutwe discarded this scope means in Future if we resolve
UserSessionwe get the new instance as shown in the console:
For more information about scopes see this link. We didn’t cover the weak scope type.
Registration Keys → A registration of the component for a given service is stored in a container with an internally created key. The container uses the key when trying to resolve a service dependency.
Value Types injection → All previous examples we saw uses classes but can resolve Struct as well Note: weak scope will not be available on Value types.
Modularizing Service Registration
Sometime in future, you can see this in your code if you don’t properly modularize your service registration as shown in Figure 16. This is where Assembler comes into play:
Assembly is a protocol that is provided a shared
Container where service definitions can be registered. Let’s create Assembly to modularize and make readable service registration process as shown in Figure 17 and 18. Note: Any assembly can resolve any other assemble object if they have the same Assembler:
Assembler is responsible for managing the
Assembly instances and the
Container. If all the above Assemblies registered with the same assembler then they have the same container which makes it possible to resolve any other container object by using this container shared instance.
How can we register Assemblies to the assembler?
As shown in Figure 19 we assembled all our assemblies into assembler where all assemblies shared the same container. Since they have the same container
ManagerAssembly can resolve
UtilityAssembly objects. Note: You MUST hold a strong reference to the
Assembler otherwise the
Container will be deallocated along with your assembler:
As shown in Figure 20 we are now able to resolve our dependency using Assembler:
As shown in Figure 21, we are resolving DateUtility in Fetcher assembly since both have the same assembler:
Swinject have many extensions SwinjectStoryboard: is one of them since it’s very useful we will cover this as well.
SwinjectStoryboard is an extension of Swinject to automatically inject dependency to view controllers instantiated by a storyboard. Very useful in VIPER architecture
You can install it through pod by adding the dependency in your podfile
As shown in Figure 22 we have a controller having class and
Where ThirdViewController has three dependencies as shown in Figure 23:
As shown in Figure 24 When ThirdViewController instantiated from storyboard
SwinjectStoryboard will resolve all the dependencies required by this controller.
Main → StoryBoard Name