Dependency injection in Swift
In this post, Efthymis examimes different approaches to implementing dependency injection in iOS.
Introduction
One of the five design principles in S.O.L.I.D. is Dependency Inversion, where the general idea of this principle is the decoupling between the high-level modules and low-level modules. One way to apply this principle is by using the Dependency Injection design pattern. The Dependency Injection makes your code more testable, more maintainable, and of course, more readable.
In this article, we will present some of the approaches to implementing the Dependency Injection in Swift.
Initializer Injection
A way to implement dependency injection is via initializer, where an object’s dependencies pass during its initialization.
class ViewController {
let apiService: APIService
let managedContext: NSManagedObjectContext
init(apiService: APIService, managedContext: NSManagedObjectContext) {
self.apiService = apiService
self.managedContext = managedContext
}
}
Example:
let apiService: APIService = APIService()
let managedContext: NSManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let viewController: ViewController = ViewController(apiService: apiService, managedContext: managedContext)
With this approach, the dependencies can become mandatory and immutable. Also, it is clear what the dependencies are and forces the developer to pass them. On the other hand, as the dependencies increasing, it leads to an initializer with many parameters.
Property Injection
Another type of dependency injection is property injection, where an object’s dependencies pass after its initialization from its properties.
class ViewController {
var apiService: APIService?
var managedContext: NSManagedObjectContext?
}
Example:
let apiService: APIService = APIService()
let managedContext: NSManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let viewController: ViewController = ViewController()
viewController.apiService = apiService
viewController.managedContext = managedContext
That is a good approach if you have optional dependencies because you don’t force the developer to pass them through the initializer. However, it is vulnerable to an object’s dependencies changes. The places where passing an object’s dependencies by property scatter in the code. When you add a new dependency, you have to find all these places and set the new dependency. Besides, since the dependencies by design are mutable, dependencies can change more than once outside the object. Thus you cannot be sure they are not replaced during the lifetime of the object. Lastly, since the dependencies by design are variables, during compilation won’t appear any warnings if it’s nil. Therefore you cannot be sure the dependencies are set.
Method Injection
Another type of dependency injection is method injection, where an object’s dependencies are injected as parameters on a function.
class ViewController {
func functionality(usesApiService apiService: APIService) {
// apiService usage
}
func functionality(usesManagedContext managedContext: NSManagedObjectContext) {
// managedContext usage
}
}
Example:
let apiService: APIService = APIService()
let managedContext: NSManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let viewController: ViewController = ViewController()
viewController.functionality(usesApiService: apiService)
viewController.functionality(usesManagedContext: managedContext)
That is also a good approach if you have optional dependencies because you don’t force the developer to pass them through the initializer. Also, another positive is that there are no stored dependency properties. Nevertheless, since the function is not private, it can be called more than once outside the object. Thus it is vulnerable to unnecessary calls of the function and can easily lead to spaghetti code.
Dependency Injection with Protocols
Another approach is by implementing protocols that describe an object’s dependencies.
protocol APIServiceDependency {
var apiService: APIService { get }
}
protocol ManagedObjectContextDependency {
var managedContext: NSManagedObjectContext { get }
}
Then in the object, describe its dependencies via Typealias and protocol composition and then passing them during its initialization, like in Initializer Injection.
class ViewController {
typealias Dependencies = APIServiceDependency & ManagedObjectContextDependency
private let dependencies: Dependencies
init(dependencies: Dependencies) {
self.dependencies = dependencies
}
}
The next step is to create a struct for your object’s dependencies and add every dependency to confirm the declared protocol composition. Like this, your code becomes more modular.
struct ViewControllerDependencies {}
extension ViewControllerDependencies: APIServiceDependency {
var apiService: APIService {
return APIService()
}
}
extension ViewControllerDependencies: ManagedObjectContextDependency {
var managedContext: NSManagedObjectContext {
return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
}
}
It might lead to creating more files; however, the code becomes modular, and all you need to do to add a new dependency is to add it to Typealias describing the object’s dependencies. Also, dependencies can become mandatory and immutable. At last, it is clear what the dependencies are and forces the developer to pass them.
Conclusion
As our app for our guests scales up and new developers join our team, the code needs to be readable and scalable. To achieve that, we decided to implement Dependency Injection by describing an object’s dependencies using protocols. In this way, we can add and remove dependencies easily, avoiding an initializer with many parameters, leveraging at the same time all the other advantages of Dependency Injection.