Basics of Dependency Injection (DI) and Dependency Inversion Principle (DIP) in Swift
Before To Start
When creating code using Swift, we often come across situations like the following:
class A {
let b: B
let c: C
...
init (b: B, c: C) {
self.b = b
self.c = c
...
}
}
class B {
}
class C {
}
...
For example, A
can be a ViewModel, while B
& C
can be considered services or managers performing specific roles.
This code looks easy to write, but it is not a good one from a design perspective. If the code in B
& C
changes, we inevitably have to modify the code inside of A
as well.
This dependent relationship is known as dependency. This time, we will explore several terms related to reducing the coupling between objects and writing more flexible code.
What is Dependency Injection (DI)?
First of all, we need to know about dependency injection. It refers to the way of providing (injection) other objects or data (service) to main object (client) from outside, typically through its initializer.
Now, let’s take a look at the detailed example code.
Example #1:
import UIKit
/* Service #1 */
final class PersonA {
var name: String = "Allen"
var age: Int = 28
func introduce() {
print("My name is \(name) and I'm \(age) years old.")
}
}
/* Service #2 */
final class PersonB {
var name: String = "Eric"
var age: Int = 15
func introduce() {
print("My name is \(name) and I'm \(age) years old.")
}
}
/* Client */
final class PersonInfo {
var person: PersonA // ⭐️ PersonA type
init(person: PersonA) { // ⭐️ PersonA type
self.person = person
}
func printName() {
print(person.name)
}
func printAge() {
print(person.age)
}
func printIntroduction() {
self.person.introduce()
}
}
let personA = PersonA()
let personB = PersonB()
/* Injection */
let personInfo = PersonInfo(person: personA)
personInfo.printName()
personInfo.printAge()
personInfo.printIntroduction()
Example #1 illustrates a situation where the class PersonInfo
depends on either PersonA
or PersonB
through the variable person
.
The PersonInfo
class can hold an instance of either PersonA
or PersonB
through its internal variable person
. In other words, the dependency occurs in the PersonInfo
class.
PersonInfo
class currently depends on an instance of PersonA
. But If we want to replace it with an instance of PersonB
, which part needs to be modified in the code above? My answer would be…
var person: PersonA = PersonA()
→var person: PersonB = PersonB()
init(person: PersonA)
→init(person: PersonB)
let personInfo = PersonInfo(person: personA)
→let personInfo = PersonInfo(person: personB)
Although Example #1 is a very simple and short code, if this were a large-scale project, manually finding and replacing these codes in the PersonInfo
class would be an impractical and foolish task.
Implementation of Dependency Inversion Principle (DIP) for More Flexible Codes
Now, we feel that Example #1 is somewhat inefficient. Then, how can we bring about changes to such dependency?
The answer lies in leveraging the dependency inversion principle, which is one of the five basic principles (SOLID) of object-oriented programming design.
It states that abstractions should not depend on details, but details should depend on abstractions. In Swift, abstractions can be represented by protocols. We can utilize protocols to newly implement dependency injection.
Let’s now take a look at the Example #2 that has been modified compared to Example #1.
Example #2:
import UIKit
/* Protocol */
protocol AnyPerson {
var name: String { get }
var age: Int { get }
func introduce()
}
/* Service #1 */
final class PersonA: AnyPerson { // ⭐️ Conform to AnyPerson protocol
var name: String = "Allen"
var age: Int = 28
func introduce() {
print("My name is \(name) and I'm \(age) years old.")
}
}
/* Service #2 */
final class PersonB: AnyPerson { // ⭐️ Conform to AnyPerson protocol
var name: String = "Eric"
var age: Int = 15
func introduce() {
print("My name is \(name) and I'm \(age) years old.")
}
}
/* Client */
final class PersonInfo {
var person: Person // ⭐️ AnyPerson protocol type
init(person: Person) { // ⭐️ AnyPerson protocol type
self.person = person
}
func printName() {
print(person.name)
}
func printAge() {
print(person.age)
}
func printIntroduction() {
self.person.introduce()
}
}
let personA = PersonA()
let personB = PersonB()
/* Injection */
let personInfo = PersonInfo(person: personA)
personInfo.printName()
personInfo.printAge()
personInfo.printIntroduction()
This time, we declare a protocol called AnyPerson
, and both the PersonA
and PersonB
classes conform to this protocol.
The PersonInfo
class has a person
variable of type AnyPerson
protocol. Since both PersonA
and PersonB
classes adopt the AnyPerson
protocol, their instances can be stored in the person
variable.
Using this approach, the person
variable can hold an instance of either PersonA
or PersonB
without change of its type. In other words, there is no need to modify many parts of the code inside the PersonInfo
class when we want to change the value of person
.
In Example #1, the PersonInfo
class was tightly coupled to either PersonA
or PersonB
by its internal code.
However, in Example #2, the PersonInfo
class conforms to the AnyPerson
protocol, and PersonA
or PersonB
depends on this AnyPerson
protocol, resulting in the inversion of control.
If we want to replace the dependency from an instance of PersonA
with an instance of PersonB
in the PersonInfo
class, what needs to be modified in the code? My answer would be…
let personInfo = PersonInfo(person: personA)
→let personInfo = PersonInfo(person: personB)
Now, we only need to modify the code outside the PersonInfo
class. By changing just a single variable where the dependency is injected, we can make our codes more flexible and independent.
Summary
- Dependency Injection (DI) is a design pattern and technique that allows objects to receive their dependencies from external sources, rather than creating or managing them internally.
- Dependency Inversion Principle (DIP) means that both high-level (abstract) and low-level (specific) modules should depend on abstraction which is represented by protocols in Swift.
- This enables more flexible and reusable code that can be easily extended or modified without affecting the rest of the codebase.
- By following the DIP, you can write Swift code more modular, scalable, and resilient to changes in requirements.
I’m a beginner learning Swift to become an iOS developer.
Support and comments are always welcome.