"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 simpler terms, DIP encourages us to avoid tight coupling between high-level and low-level components by making both depend on abstractions (like interfaces or abstract classes) instead of concrete implementations. This leads to more flexible and scalable designs, as changes to low-level components won’t require changes to high-level components.
Imagine you are building a notification system that sends alerts via different channels such as email and SMS. Without dependency injection or the Dependency Inversion Principle, your NotificationService class might depend directly on concrete implementations like EmailSender or SMSSender. This creates a tight coupling between the high-level class (NotificationService) and the low-level classes (EmailSender, SMSSender).
// Low-level module: EmailSender
class EmailSender {
public void sendEmail(String message) {
System.out.println("Sending email with message: " + message);
}
}
// Low-level module: SMSSender
class SMSSender {
public void sendSMS(String message) {
System.out.println("Sending SMS with message: " + message);
}
}
// High-level module: NotificationService
class NotificationService {
private EmailSender emailSender;
private SMSSender smsSender;
public NotificationService() {
this.emailSender = new EmailSender(); // Tight coupling to low-level module
this.smsSender = new SMSSender(); // Tight coupling to low-level module
}
public void sendNotification(String message) {
emailSender.sendEmail(message);
smsSender.sendSMS(message);
}
}
// Client code
public class Main {
public static void main(String[] args) {
NotificationService notificationService = new NotificationService();
notificationService.sendNotification("Hello, Dependency Inversion Principle!");
}
}
// High-level abstraction: MessageSender interface
interface MessageSender {
void send(String message);
}
// Low-level module: EmailSender
class EmailSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("Sending email with message: " + message);
}
}
// Low-level module: SMSSender
class SMSSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("Sending SMS with message: " + message);
}
}
// High-level module: NotificationService (now depends on abstraction)
class NotificationService {
private MessageSender messageSender;
// Constructor injection (dependency injection)
public NotificationService(MessageSender messageSender) {
this.messageSender = messageSender; // Depends on abstraction, not concrete implementation
}
public void sendNotification(String message) {
messageSender.send(message);
}
}
// Client code demonstrating Dependency Injection
public class Main {
public static void main(String[] args) {
// Injecting EmailSender into NotificationService
MessageSender emailSender = new EmailSender();
NotificationService emailNotification = new NotificationService(emailSender);
emailNotification.sendNotification("Hello via Email!");
// Injecting SMSSender into NotificationService
MessageSender smsSender = new SMSSender();
NotificationService smsNotification = new NotificationService(smsSender);
smsNotification.sendNotification("Hello via SMS!");
}
}