Dependency Inversion Principle (DIP)

Dependency Inversion Principle (DIP)

Photo by Sven Mieke on Unsplash

The class should depend on interfaces rather than a concrete class

The Dependency Inversion Principle (DIP) is one of the five SOLID principles of object-oriented programming and design.

Inversion: Assume Class A relies on Class B, and Class B is reliant on Class C. By employing the principle of dependency inversion, the relationship transforms: Class A now relies on Interface B and Interface B is structured to be dependent on Interface C. Simultaneously, Class B's dependence shifts to Interface B, and Class C's reliance aligns with Interface C.

The goal of Dependency Inversion is to invert the direction of dependencies, so high-level modules/classes (like Class A) should not depend on low-level modules/classes (like Class B), but both should depend on abstractions (interfaces or abstract classes).

The subsequent assertions are applicable within the framework of DIP. These four statements convey identical meanings, and reviewing them could prove beneficial.

I. The class should depend on interfaces rather than a concrete class:

This statement emphasizes the importance of programming to interfaces rather than specific implementations. By relying on interfaces, a class becomes less tightly coupled to a particular implementation, making it easier to switch out implementations without affecting the client code.

Example:

Consider a scenario where you have a NotificationService class that sends notifications to users via various methods (email, SMS, etc.). Instead of directly depending on concrete implementations like EmailNotifier and SMSNotifier, NotificationService should depend on an interface, say INotifier, which both EmailNotifier and SMSNotifier implement. This allows you to add new notification methods without modifying NotificationService.

interface INotifier {
    void sendNotification(String message);
}

class EmailNotifier implements INotifier {
    public void sendNotification(String message) {
        // Code to send email notification
    }
}

class SMSNotifier implements INotifier {
    public void sendNotification(String message) {
        // Code to send SMS notification
    }
}

class NotificationService {
    private INotifier notifier;

    public NotificationService(INotifier notifier) {
        this.notifier = notifier;
    }

    public void sendNotification(String message) {
        notifier.sendNotification(message);
    }
}

II. High-level modules should not depend on low-level modules:

This statement suggests that both high-level and low-level modules should depend on abstractions, rather than depending on each other directly. High-level modules are the ones that deal with higher-level logic, while low-level modules handle implementation details.

Example:

Imagine an e-commerce system where the OrderProcessor (high-level module) processes orders and needs to calculate the total price. Instead of directly depending on a Product class (low-level module), should depend on an abstraction like ProductInterface. This way, changes in the Product implementation won't directly impact the OrderProcessor.

interface ProductInterface {
    double getPrice();
}

class Product implements ProductInterface {
    private double price;

    public Product(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }
}

class OrderProcessor {
    private ProductInterface product;

    public OrderProcessor(ProductInterface product) {
        this.product = product;
    }

    public double calculateTotalPrice(int quantity) {
        return product.getPrice() * quantity;
    }
}

III. Abstraction should not depend on implementation:

This statement emphasizes that abstractions should not be tied to specific implementations. The definition of an interface or abstraction should be independent of how it's implemented.

Example:

If you define an INotifier interface for the notification system, shouldn't have any knowledge of the concrete classes like EmailNotifier or SMSNotifier. Its purpose is to define the methods and contracts that notifiers should adhere to, leaving the actual implementation details to the concrete classes.

IV. Implementation should depend on abstraction:

This final statement underlines that the concrete implementations of modules or classes should be built upon the abstractions they depend on, rather than the other way around.

Example:

The EmailNotifier and SMSNotifier classes should implement the methods defined in the INotifier interface. This ensures that the implementation is consistent with the contract established by the abstraction, allowing the high-level modules to interact with them interchangeably.

Conclusion: It is prudent to establish interfaces for modules or classes at each level of hierarchy. When a class aims to make use of objects from other classes, it is wise to employ interfaces tailored to those classes. It is essential that interfaces do not include methods that incorporate concrete classes in their signatures or prototypes.