The Dependency Inversion Principle (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.
Play explanatory video
In simpler terms... High-level classes (those that contain the business logic) should not depend on low-level classes (those that handle specific details like the database or sending emails). Both should depend on interfaces or abstract classes.
# Inversion, Abstractions, and Details?
Inversion: Changing the traditional direction of dependencies in object-oriented programming.
Abstractions: Interfaces or abstract classes that define behavior contracts.
Details: Specific and concrete parts of a system's implementation, such as Database, User Interface components.
The Dependency Inversion Principle aims to reduce coupling between high-level code (policies, decisions) and low-level code (details, implementations), leading to more flexible and maintainable systems.
# Why is it important?
Flexibility
Facilitates changing implementation details without affecting high-level code.
Reusability
High-level modules become more reusable by not being tied to specific implementations.
Maintainability
Improves maintainability by decreasing coupling between different parts of the code.
# Symptoms of violation
We can identify when we are not respecting the Dependency Inversion Principle:
- High-level code is directly linked to specific low-level implementations.
- Changes in low-level implementation details affecting high-level code.
- Difficulty changing or swapping low-level components due to rigid dependencies.
Example
Let's imagine a notification system for an application.
Without Dependency Inversion Principle
The notification system is directly coupled to a specific messaging implementation.
public class NotificationService { private EmailService emailService; public NotificationService() { this.emailService = new EmailService(); // Direct coupling } public void sendNotification(User user, String message) { emailService.sendEmail(user, message); } }
Switching to another notification method would require modifying
NotificationService.
With Dependency Inversion Principle
We define an abstraction for message sending and make NotificationService
depend on this abstraction.
public interface MessageService { void sendMessage(User user, String message); } public class EmailService implements MessageService { sendMessage(User user, String message) { // Specific implementation for sending an email } } public class NotificationService { private MessageService messageService; public NotificationService(MessageService service) { } public void sendNotification(User user, String message) { messageService.sendMessage(user, message); } }
Conclusions
By implementing the Dependency Inversion Principle (DIP),
NotificationService
does
not depend directly on EmailService
but on the MessageService
abstraction.
This means we can change how messages are sent (for example, to SMS or push notifications)
without having to modify NotificationService.
Summary
The Dependency Inversion Principle is vital for creating flexible and decoupled systems. It encourages high-level modules to not depend on low-level ones, but on abstractions, facilitating future changes and improving maintainability. DIP promotes a robust software design, where modifications to implementation details have a minimal impact on high-level code, making the system more adaptable and easier to evolve.