๐ Introduction
In a good code design is very important to use proper abstractions because they make our code loosely coupled. This means that different components of our code can be replaced with alternative implementations without affecting other parts.
Loosely coupled code is the main goal of Dependency Injection. It allows us to write code that is easier to test, extend, and reuse. All of this makes our code easier to maintain.
The Dependency Injection design pattern aims to reduce dependencies between components by allowing dependencies to be injected from the outside rather than being created or managed internally.
Dependency injection cannot be understood without the Dependency Inversion principle. In simple words, this principle states that implementation details should depend on higher-level abstractions and this is essential for creating loosely coupled applications.
In the Dependency Injection design pattern, dependencies are "injected" into a class through constructor injection
, property injection
, method injection
, or ambient context
๐ท Types of Injection
1. Constructor Injection
: Dependencies are passed to a class through its constructor.
2. Property Injection
: Dependencies are set through properties or variables of a class.
3. Method Injection
: Dependencies are provided through method parameters.
4. Ambient Context
: Single globally accessible dependency that is exposed and can be used by a lot of different clients in an app.
๐จ Diagram
๐จ๐ผโ๐ป Implementation
In the following example, we are going to see the most suggested and used use-case of the Dependency Injection through the constructor (initializer).
We have a DataManager
class that depends on a DataSourceProtocol
.
The DataManager
is decoupled from specific implementations of the DataSourceProtocol
, making it more flexible and easier to test.
We provide the appropriate implementation of the DataSourceProtocol
during the initialization of the DataManager
using in this case the constructor injection
.
// A protocol defining a data source
protocol DataSourceProtocol {
func fetchData() -> String
}
// A concrete implementation of the DataSource protocol
class RemoteDataSource: DataSourceProtocol {
func fetchData() -> String {
return "Data from remote server"
}
}
// Another concrete implementation of the DataSource protocol
class LocalDataSource: DataSourceProtocol {
func fetchData() -> String {
return "Data from local storage"
}
}
// A class that depends on the DataSource protocol and
// injects it using constructor injection
class DataManager {
private let dataSource: DataSourceProtocol
init(dataSource: DataSourceProtocol) {
self.dataSource = dataSource
}
func displayData() {
let data = dataSource.fetchData()
print("Fetched data: \(data)")
}
}
To use the DataManager
in from our code example, you can follow these steps:
// Create instances of the data sources
let remoteDataSource = RemoteDataSource()
let localDataSource = LocalDataSource()
// Create an instance of DataManager with a remote data source
let dataManager = DataManager(dataSource: remoteDataSource)
dataManager.displayData()
// Fetched data: Data from remote server
// Create another instance of DataManager with a local data source
let anotherDataManager = DataManager(dataSource: localDataSource)
anotherDataManager.displayData()
// Fetched data: Data from local storage
โ Positive aspects
1. Testability
: With Dependency Injection, it's easy to provide mock or stub implementations of dependencies during testing, allowing for comprehensive unit testing.
2. Modularity and reusability
: By injecting dependencies, components become more modular and can be reused in different contexts or scenarios.
3. Flexibility
: Dependency Injection makes it easier to switch or substitute dependencies without modifying the consuming class. This promotes flexibility and makes the code more maintainable.
โ Negative aspects
1. Increased complexity
: Introducing Dependency Injection can add complexity to the codebase, especially in larger projects. Managing dependencies, their lifecycles, and configuration can become challenging.
2. Indirect dependencies
: Dependency Injection can result in a chain of dependencies, making it harder to trace and understand the flow of data throughout the application.
3. Increased setup and boilerplate code
: Dependency Injection often requires writing additional code for dependency management, configuration, and injection, which can increase the overall codebase size.
๐ Conclusions
Dependency Injection is a powerful design pattern that promotes loose coupling, testability, and modularity.
By allowing dependencies to be injected from the outside, it enhances flexibility and makes the code more maintainable.
However, it's important to strike a balance and carefully consider the complexity and overhead it introduces to ensure the benefits outweigh the drawbacks.
The specific implementation and usage of Dependency Injection can vary based on the requirements and architectural choices of your iOS project.
If you want to be notified of the upcoming articles you can subscribe to the Newsletter and support The iOS Mentor blog using Buy Me a Coffee.
You also have the option to support this blog as a sponsor, contributing to the growth of our iOS Development community.
I am also available on LinkedIn and GitHub so let's connect!
Thanks for reading everyone and enjoy the rest of your day! ๐