High-level modules should not depend on low-level modules. Both should depend on abstractions.
The Dependency Inversion Principle (DIP) is a fundamental concept in object-oriented design that promotes loose coupling between software modules. It consists of two key parts:
In simpler terms, DIP suggests that instead of high-level components directly using low-level components, both should rely on a common abstraction. This "inverts" the traditional dependency flow where high-level modules directly depend on low-level ones.
Consider a NotificationService
that directly depends on concrete EmailSender
and SMSSender
classes.
// Low-level Modules (Concrete Implementations)
class EmailSender {
send(message, recipient) {
console.log(`Sending email to ${recipient}: ${message}`);
}
}
class SMSSender {
send(message, recipient) {
console.log(`Sending SMS to ${recipient}: ${message}`);
}
}
// High-level Module
class NotificationService {
constructor() {
// Direct dependency on concrete low-level modules
this.emailSender = new EmailSender();
this.smsSender = new SMSSender();
}
sendNotification(type, message, recipient) {
if (type === 'email') {
this.emailSender.send(message, recipient);
} else if (type === 'sms') {
this.smsSender.send(message, recipient);
} // Adding new notification types requires modifying this class
}
}
// Usage
const notifier = new NotificationService();
notifier.sendNotification('email', 'Hello via Email!', 'user@example.com');
// Problem: NotificationService is tightly coupled to EmailSender and SMSSender.
// Adding a PushNotificationSender would require changing NotificationService.
Here, the high-level NotificationService
directly creates and uses instances of the low-level EmailSender
and SMSSender
. This tight coupling makes it difficult to add new notification methods (like push notifications) without modifying NotificationService
.
We introduce an abstraction (IMessageSender
interface) that both high-level and low-level modules depend on. Dependencies are injected instead of being created internally.
// Abstraction (Interface)
// Conceptual in JS, often implemented using duck typing or explicit checks
/*
interface IMessageSender {
send(message, recipient);
}
*/
// Low-level Modules implementing the abstraction
class EmailSender /* implements IMessageSender */ {
send(message, recipient) {
console.log(`Sending email to ${recipient}: ${message}`);
return `Email sent to ${recipient}.`;
}
}
class SMSSender /* implements IMessageSender */ {
send(message, recipient) {
console.log(`Sending SMS to ${recipient}: ${message}`);
return `SMS sent to ${recipient}.`;
}
}
class PushNotificationSender /* implements IMessageSender */ {
send(message, recipient) {
console.log(`Sending push notification to ${recipient}: ${message}`);
return `Push notification sent to ${recipient}.`;
}
}
// High-level Module depending on the abstraction
class NotificationService {
// Dependency is injected via the constructor
constructor(messageSenders) { // Expects an object/map of sender instances
this.messageSenders = messageSenders;
}
sendNotification(type, message, recipient) {
const sender = this.messageSenders[type];
if (sender && typeof sender.send === 'function') {
return sender.send(message, recipient);
} else {
console.error(`Notification type '${type}' not supported or sender is invalid.`);
return `Failed to send: type '${type}' not supported.`;
}
}
}
// Usage (Dependency Injection)
const senders = {
email: new EmailSender(),
sms: new SMSSender(),
push: new PushNotificationSender() // Easily add new senders
};
const notifier = new NotificationService(senders);
notifier.sendNotification('email', 'DIP rocks!', 'dev@example.com');
notifier.sendNotification('push', 'New feature deployed!', 'user-device-token');
// NotificationService depends on the IMessageSender abstraction,
// not on concrete implementations.
Now, NotificationService
depends on the IMessageSender
abstraction (conceptually). Concrete sender instances are *injected* (passed in), often via the constructor or a setter method (Dependency Injection). This decouples the high-level module from low-level details, allowing new sender types to be added easily without modifying NotificationService
itself.
Select a payment method and place an order. The OrderProcessor
depends on a IPaymentGateway
abstraction, allowing different payment methods (low-level details) to be plugged in.