Usability of SOLID Principles with Kotlin - Dependency Inversion Principle: Part 5
- Thalles Vieira
- 5 de jun. de 2024
- 3 min de leitura
We have reached the end of the 5 SOLID principles, and we will address the last one in this article.

The Dependency Inversion Principle (DIP) is the last of the five SOLID principles. Introduced by Robert C. Martin, the DIP aims to reduce the coupling between high-level and low-level modules, promoting a more flexible and easier-to-maintain architecture.
Definition of DIP
The DIP states that:
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
"Abstractions should not depend on details. Details should depend on abstractions."
In other words, the DIP suggests that we should focus on the high-level module, making it independent or decoupled, and the low-level modules should depend on it through abstractions, such as interfaces.
For example, in an application that does not use this principle, one layer may depend on the implementation of another, creating coupling between them, as shown in the image below. This makes the web layer dependent on the implementations of the domain layer, and the domain layer dependent on the implementations of the persistence layer.

Now, in this second example, if we use dependency inversion, making the lower-level layers (web and persistence) depend on interfaces to communicate with the domain layer, which is the heart of the application, we achieve decoupling of these dependencies, making the architecture more flexible.

Importance of DIP
Reduces Coupling: By depending on abstractions, the DIP allows modules to be independent of their concrete implementations, facilitating the exchange of implementations without affecting the dependent code.
Eases Maintenance: With fewer direct dependencies between modules, it is easier to maintain and evolve the system, as changes in one part of the code do not affect other parts.
Promotes Testability: Depending on abstractions, it is easier to substitute real components with mocks or stubs during testing, improving the system's testability.
Improves Flexibility: Systems that follow the DIP are more flexible and can adapt to requirement changes more quickly, as dependencies are easily replaceable.
Now, let's illustrate this with code to facilitate understanding. Consider an example where we have a notification system that sends messages through different channels, such as email and SMS. Without the DIP, high-level classes depend directly on low-level classes.
Without DIP (Problem)
class EmailService {
fun sendEmail(message: String) {
println("Sending email: $message")
}
}class SMSService {
fun sendSMS(message: String) {
println("Sending SMS: $message")
}
}class NotificationSender {
private val emailService = EmailService()
private val smsService = SMSService()
fun sendNotification(message: String) {
emailService.sendEmail(message)
smsService.sendSMS(message)
}
}
In this example, NotificationSender depends directly on EmailService and SMSService, creating strong coupling and making the code less flexible and testable.
Applying the DIP
Let's introduce abstractions to break the direct dependencies between NotificationSender and the notification services.
interface NotificationService {
fun send(message: String)
}
class EmailService : NotificationService {
override fun send(message: String) {
println("Sending email: $message")
}
}
class SMSService : NotificationService {
override fun send(message: String) {
println("Sending SMS: $message")
}
}
class NotificationSender(private val notificationServices: List<NotificationService>) {
fun sendNotification(message: String) {
notificationServices.forEach { it.send(message) }
}
}
Implementation of Specific Classes
fun main() {
val emailService = EmailService()
val smsService = SMSService()
val notificationSender = NotificationSender(listOf(emailService, smsService))
notificationSender.sendNotification("Hello, this is a test notification.")
}
In this example, NotificationSender depends on the abstraction NotificationService instead of directly depending on EmailService and SMSService. This allows new notification services to be added easily without modifying NotificationSender.
Conclusion
The Dependency Inversion Principle (DIP) is crucial for creating flexible, modular, and easy-to-maintain software systems. By depending on abstractions instead of concrete implementations, the DIP reduces coupling, facilitates maintenance, improves testability, and promotes system flexibility. In Kotlin, the DIP can be efficiently applied, resulting in more robust and adaptable code that can easily accommodate changing requirements.




Comentários