Dependency Injection simply means "injecting" or passing the dependencies that one class needs in order to perform its tasks.
For example, we have Class A that is dependent on Class B to do something, instead of declaring a variable that will initialize our dependency inside our class, we can "inject" these dependencies, in this case, we can inject Class B to Class A.
This is a very common setup if you are new in programming and aren't familiar with Dependency Injection, you might do something like this. I admit, I did something like this, too.
final class MyAPIClient: APIClient {
func execute() async throws -> Result<Void, Error>
}
final class MyRepository {
let apiService = MyAPIClient() //dependency is created inside the dependent class
func someFunction() {
let result = try await apiService.execute()
...
}
}
MyRepository needs (or is dependent on) MyAPIClient to perform a task, and assuming it is to call an API function. If you noticed in the code above, we created a variable apiService and initialized MyAPIClient inside MyRepository class.
With this example, it is difficult to create unit tests for MyRepository class because it will be directly calling the class we are dependent of, which is calling an API. Of course, we wouldn't want to call the API directly when testing. But with dependency injection and the use of protocols, we can test MyRepository class.
This also makes it tightly coupled. Because Class A depends on Class B, changes on the Class B will affect Class A. MyRepository class doesn't need to know MyAPIClient class. But how can MyRepository call the API functions if not with the help of MyAPIClient class?
Dependency Injection and the use of protocols / interfaces can solve the issues above. Dependency Injection is a great way to decouple our classes and it makes our codes scalable and testable.
With Dependency Injection, I'd like to assign the classes involved with specific roles. We have the "Builder," the "Dependent" and the "Resource/Dependency."
Types of Dependency Injections
First, let's identity the different types of Dependency Injections. But please take note that in my first few examples, I will not be using protocols, BUT we will update these codes later to be able to show you how protocols/interfaces can help us in testing our codes.
Constructor Injection - Passing your dependencies via constructors
final class MyRepository {
private var service: MyAPIClient
init(service: MyAPIClient) {
self.service = service
}
}
In this example, our Resource/Dependency, which is the MyAPIClient is being injected into the Dependent class, MyRepository via constructor / initialization.
The Builder is the one that creates an instance of the dependency, and then injects it to the dependent class as seen above. So our Dependent class must provide a constructor that will allow the Builder to pass the dependencies needed for that class.
Setter Injection - Passing your dependencies via setter methods
final class MyRepository {
private var service: MyAPIClient
func setServiceClient(_ service: MyAPIClient) {
self.service = service
}
}
In this example, our Resource/Dependency, which is the MyAPIClient is being injected into the Dependent class, MyRepository via setter method.
Here, our Dependent class must provide a setter method(s) that will allow the Builder to pass the dependencies needed for that class. If you have more dependencies, you can create more setter methods to set that specific dependency.
Property Injection - Passing your dependencies via properties
Annotation Injection - when using third-party libraries like Resolver (on iOS), we can create our dependencies and then use the property wrapper provided by the library to inject our dependencies.
Instead of injecting via Constructor/Setter, you can now inject the dependencies like this.
final class MyRepository {
@Injected private var service: MyAPIClient
// If you're using protocols
// @Injected private var service: MyAPIClientProtocol
}
This means that Resolver will "resolve" or "inject" this dependency. And in order for it to do that, you have to register that class so that Resolver will know which dependency to inject. Here's an example:
import Resolver
extension Resolver: ResolverRegistering {
public static func registerAllServices() {
register { MyAPIClient() }.scope(.unique)
register { MyAPIClient() as MyAPIClientProtocol }
}
}
There are 5 different scopes, too: Graph (default), Application, Cached, Shared and Unique. And you can set the scope like this:
register { dependency }.scope(.application) // the same as a singleton
register { dependency }.scope(.graph) // creates new instance
register { dependency as ProtocolName } // uses default scope
My Personal Opinion
And those are the different types of Dependency Injection. But before I end this post, I just want to share my personal opinion about each type of injections.
With Setter and Property Injections, I don't really recommend using them as it's so easy to forget that we have to set these dependencies by calling the setter methods, or by passing the dependency through the variable. And if you forget to set it, you'll possibly get crashes at run time, or bugs or weird behavior in your app.
Unlike Constructor Injection, you have no choice but to pass these dependencies the moment you initialize your class, so there's no way you'll forget passing your dependencies. But, this also means you have to keep setting these dependencies every time you call this particular class.
Example, every time you need to use MyRepository, you'll have to call it like this. Imagine if your class has a lot of dependencies, you constructor will be bloated, and calling this class with so many injected dependencies will look kinda ugly. But of course, this is still better than not using dependency injection at all.
let repository = MyRepository(service: MyAPIClient())
However, with Annotation Injection, it's already done for you the moment you declare these dependencies when you create your class (and as long as these dependencies were registered, if not, you'll get a crash). In this example:
final class MyRepository {
@Injected private var service: MyAPIClient
func doSomething() {
service.execute()
}
}
You won't be able to call the execute() function from your dependency class without declaring it first. And because you already declared it, the library will supply an instance of your dependency to your class, you no longer have to concern your self in passing these dependencies, the library (like Resolver) will do it for you. The Annotation Injection fixes all the concerns I have listed above.
In the next post, we will update our examples here, we'll also use protocols and also tackle how dependency injection makes our classes testable. We will show some unit testing and mocking our dependencies.
Comments