Dependency Injection and Dependency Inversion

Janvi Arora
5 min readApr 23, 2023

--

In Swift, dependency injection is commonly used to make objects more modular, testable, and reusable. Instead of creating dependencies inside an object, dependencies are injected into the object through initializers or methods. This allows for greater flexibility in changing and testing dependencies.

It’s the process in which we create object of class A into class B that depends on that object.

Dependency to call B( )

Obviously, the above code is going to work, but this violates the dependency injection principle. Moreover, this code is going to create tight coupling.

To avoid this, dependency injection says that dependency has to be injected and not created inside a class.

Types of dependency injection:

  1. Constructor injection
  2. Property injection
  3. Method injection

Constructor injection:

Here, the dependencies of a class are passed in through its constructor. In Swift, this means that the dependencies are declared as parameters of the class’s initializer.

class DataManager {
let networkService: NetworkService

init(networkService: NetworkService) {
self.networkService = networkService
}

func fetchData() {
// Use the network service to retrieve data
// ...
}
}

In this example, the DataManager class takes in a NetworkService instance through its initializer, and saves it to a property. The fetchData method can then use the networkService property to retrieve data.

Constructor injection allows the DataManager class to be easily testable, since we can provide a mock NetworkService instance in tests. It also makes the dependencies of the class explicit and easily discoverable, which can help with maintainability and understanding of the codebase.

Property Injection:

In property injection, the dependencies are passed to the object through properties. Unlike constructor injection, properties are set after the object is created, so they can be optional or have default values.

class MyViewController: UIViewController {
var myDependency: MyDependency?

override func viewDidLoad() {
super.viewDidLoad()

// Use myDependency
myDependency?.doSomething()
}
}

In this example, MyViewController has a property myDependency which is of type MyDependency. The dependency is not required to be passed in the initializer, and it can be set at any time. When viewDidLoad() is called, myDependency is used to perform some action.

To use property injection, you need to make sure that the dependency property is optional and has a default value, or that it is explicitly set before being used. Also, it’s important to avoid using force unwrapping when accessing the property, as it can cause crashes if the property is not set.

Method Injection:

Method injection is another approach for dependency injection in Swift. In this approach, a method is defined with a parameter that represents the dependency. The dependency is passed to the method when it is called, which allows the method to use the dependency.

class MyViewController: UIViewController {
private let apiClient: APIClient

init(apiClient: APIClient) {
self.apiClient = apiClient
super.init(nibName: nil, bundle: nil)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func fetchData() {
apiClient.getData { result in
switch result {
case .success(let data):
// handle successful data retrieval
case .failure(let error):
// handle error
}
}
}
}

In the example above, the MyViewController class has a private property apiClient of type APIClient. The init method takes an apiClient parameter and assigns it to the apiClient property. This is an example of constructor injection.

The fetchData method takes no parameters, but it uses the apiClient property to retrieve data from the API. This is an example of method injection.

NOTE: In general, constructor injection is preferred over property and method injection in Swift, because it ensures that all required dependencies are available at the time of object creation and makes it clear what dependencies a class needs to function. With constructor injection, the dependencies are explicitly defined and easily visible in the class’s initializer.

Issues with Dependency Injection:

We will be able to access all the properties of the object that will be created. This will lead to the breakage of abstraction rule.

Solution:

  1. Make the methods that are not required outside the class as private. (But sometimes a developer may forget to initiate a method as private, which will loop us to the same issue)

2. Making use of protocols to pass the function or data required. (Dependency inversion principle)

Dependency Inversion:

Dependencies must always be passed using the highest level of abstraction i.e. protocols.

Dependency Inversion is a design principle that states that high-level modules should not depend on low-level modules, but both should depend on abstractions. This is achieved through the use of abstractions or protocols to define the required functionality and decouple the high-level and low-level modules.

Let’s take an example to understand this better. Suppose we have an application that requires the functionality to send email notifications. We have a high-level module NotificationService that is responsible for sending notifications, and a low-level module EmailSender that is responsible for sending emails. In a non-inverted dependency scenario, NotificationService directly depends on EmailSender.

class NotificationService {
let emailSender = EmailSender()
...
}

In this case, if we need to change the email sending functionality or add new functionality, we would have to modify NotificationService, which is not ideal. Instead, we can invert the dependency and define an abstraction or protocol EmailSending that defines the required functionality. The EmailSender class can then implement this protocol, and NotificationService can depend on the protocol abstraction rather than the concrete EmailSender class.

protocol EmailSending {
func sendEmail(to: String, subject: String, body: String)
}

class EmailSender: EmailSending {
func sendEmail(to: String, subject: String, body: String) {
// send email using SMTP or API
}
}

class NotificationService {
let emailSender: EmailSending
init(emailSender: EmailSending) {
self.emailSender = emailSender
}
...
}

In this way, NotificationService is decoupled from the concrete EmailSender class and depends on the protocol abstraction instead. We can now easily swap out the EmailSender class with a different email sending class that also implements the EmailSending protocol without having to modify NotificationService.

Dependency Inversion helps to achieve more flexible and maintainable code by reducing the coupling between modules and promoting the use of abstractions to define the required functionality.

--

--