Select Page

Dependency injection and dependency inversion are crucial concepts in software and app development, allowing you to create code that is scalable, maintainable, and easily testable. Let’s delve into these powerful concepts to build good iOS applications.

Dependency Injection

Dependency Injection involves “injecting” a class’s dependencies from the outside, rather than having the class create them internally. This can be achieved through constructor injection or property injection.

For instance, consider the following example using constructor injection:

class FriendsViewModel{
    private let friendsLoader: FriendsLoader
    
    init(friendsLoader: FriendsLoader) {
        self.friendsLoader = friendsLoader
    }
}

Alternatively, property injection can be used, as shown below:

class FriendsViewModel{
    var friendsLoader: FriendsLoader?
}

let viewModel = FriendsViewModel()
viewModel.friendsLoader = //Inject friends loader implementation here

Using constructor injection is generally safer, as it ensures that friendsLoader remains immutable, preventing the class from being in an invalid state.

Advantages of using dependency injection are as follows:

  1. Clearly shows explicit dependencies and provides better separation of concerns.
  2. Easily swap out dependencies without altering the core logic of FriendsViewModel.
  3. Easier testing, as we can inject mock objects for testing purposes.
  4. Responsibility for creating FriendsLoader lies outside the FriendsViewModel, adhering to the Single Responsibility Principle in SOLID principles, which states that a class should have only one reason to change.

Dependency Inversion

Now that you’ve implemented dependency injection to inject dependencies into your classes, it’s crucial to consider the usage of dependency inversion.

Dependency Inversion is a principle of SOLID principles. It suggests that high-level modules or classes should not depend on low-level modules or classes directly. Instead, both should depend on abstractions (protocol).

For example,FriendsLoader in FriendsViewModel class can be either a protocol or a concrete class.

protocol FriendsLoader{
    func loadFriends() async throws -> [Friend]
}

By following SOLID principles, FriendsLoader should be a protocol, as shown above, so that FriendsViewModeldepends on abstraction. There are several reasons why this is considered good practice:

  1. FriendsViewModel should only be aware of loading friends, while the actual FriendsLoader implementation can vary, such as loading friends from a database, network, in-memory storage, or a combination of these implementations.
  2. Testing becomes much easier, as we can create stub/mock objects that conform to the FriendsLoader protocol.
  3. Encourages a modular app structure, as components are loosely coupled with each other.

Conclusion

Dependency injection and dependency inversion are vital concepts on creating a more modular and maintainable iOS apps, where components are loosely coupled and dependencies can be easily swapped without modifying existing classes. Additionally, these practices enhance code reusability and testability, contributing to the scalability of your app’s codebase.