Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Definition

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:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).
  2. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

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.

Why DIP Matters

Bad Example: Violating DIP

Scenario: Notification System

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.

Good Example: Applying DIP

Scenario: Decoupled Notification System

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.

Interactive Demo: Ordering System

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.

Select a payment method and click the button.