CLOSE

In this article, we will deep dive into the fifth and final SOLID principle of object-oriented design – the Dependency Inversion Principle (DIP). By understanding and applying DIP, you can build flexible, decoupled, and maintainable systems.

What Is Dependency Inversion Principle (DIP)?

The Dependency Inversion Principle (DIP) states:

“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.”

It states that when designing software systems, high-level modules should not depend directly on low-level modules. Instead, both high-level and low-level modules should depend on abstractions or interfaces.

In Simpler terms:

  • Don't let your high-level logic be tightly coupled to low-level implementation details.
  • Instead, both should communicate via interfaces or abstract classes (abstractions).
  • This reduces coupling and improves flexibility, extensibility, and testability.

This principle aims to reduce the coupling between high-level (business logic) and low-level (implementation details) components by introducing abstractions (interfaces or abstract classes).

🎯 Objective of DIP

DIP aims to decouple the core logic (business rules) from implementation details (such as APIs, databases, and devices) by introducing abstractions that sit in between them.

Key Concepts

TermDescription
High-Level ModulesRepresent the core business logic or policy of the application (e.g., controllers, use cases)
Low-Level ModulesContain concrete implementations or operational details (e.g., services, databases, APIs)
AbstractionsInterfaces or abstract classes that define a contract for behavior without specifying how it’s done

❗Why DIP Matters

Without DIP, your application components are tightly coupled, which leads to:

  • ❌ Fragile code – changes in one place break others. Mostly change in low-level modules can break high-level modules.
  • ❌ Difficult unit testing – hard to isolate logic
  • ❌ Poor extensibility – cannot easily switch implementations

By applying DIP:

✅ You invert the dependency direction so that both layers depend on an abstraction
✅ You can swap out low-level modules without touching core logic
✅ You gain testability, flexibility, and modularity

🏠 Real-Life Analogy: Home Entertainment System

Imagine a home entertainment system with a TV, sound system, DVD player, and gaming console.

❌ Without DIP:

Each device is directly connected to the TV:

  • DVD player to HDMI1
  • Gaming console to HDMI2
  • Sound system to audio out

Now if you replace the TV or add a new device, you must rewire and reconfigure everything – this is tight coupling.

✅ With DIP:

Introduce a Home Entertainment Controller that acts as a middle layer:

  • Devices implement a HomeEntertainmentDevice interface.
  • The controller interacts only with the interface.
  • Any device that follows the interface can be plugged in without changing controller logic.

✅ Devices depend on the abstraction, not the controller.
Controller is independent of device implementations.
✅ That's DIP in action.

Components:

  1. High-level Module (Home Entertainment Controller):
    1. The home entertainment controller serves as the high-level module. It orchestrates the interactions between various devices and the television, abstracting away the details of individual connections.
  2. Abstraction (Home Entertainment Interface):
    1. We define an interface, called the home entertainment interface, that specifies methods for controlling and interacting with the entertainment system. This interface abstracts the functionality required by the controller.
  3. Low-level Modules (Devices):
    1. Each device, such as the DVD player, gaming console, sound system, etc., implements the home entertainment interface. These devices provide specific implementations for the methods define by the interface.

Real-World Analogy: Electric Devices and Plugs

Imagine electric devices (TV, Fan, Phone charger) are high-level modules, and wall sockets are low-level modules

Instead of each device hardwiring into the wall, they all use a standard plug interface. That's DIP in real life!

Practical Applications of Dependency Inversion:

1. Dependency Injection (DI)

DIP is commonly applied using Dependency Injection techniques like:

  • Constructor Injection
  • Setter Injection
  • Interface Injection

This allows injecting dependencies from outside, rather than instantiating them inside the class. This improves testability and separation of concerns.

  • Dependency injection is a technique used to implement Dependency Inversion in practice. It involves passing dependencies (often through constructor parameters or setter methods) into a class rather than creating them internally. This enables the swapping of dependencies at runtime, making the system more flexible and testable.

2. Plugin Architecture

Using DIP, you can design plugin-based where:

  • Plugins implement a common interface
  • The core app loads and interacts with them without knowing their internal logic

This allows dynamic extension without touching the core code.

❌ Violation of DIP

Example: Direct Dependency

Let's say we are building a notification system. A Notificationclass sends an email using a concrete EmailService.

class EmailService {
public:
    void sendEmail(const string& message) {
        cout << "Sending email: " << message << endl;
    }
};

class Notification {
    EmailService emailService;
public:
    void notify(const string& message) {
        emailService.sendEmail(message);
    }
};

Why This Violates DIP:

  1. Tight Coupling: Notification depends directly on the EmailService class.
  2. Hard to Extend: Adding another service like SMS requires modifying the Notification clas.
  3. This breaks Open/Closed Principle too.

This tightly couples the high-level module (Notification) to the low-level detail (EmailService) – a clear DIP violation.

✅ Adhering to DIP

Introduce an abstraction (MessageService) to decouple Notification from specific messaging services:

🔹 Step 1: Define the abstraction

class INotificationService {
public:
    virtual void send(const string& message) = 0;
    virtual ~INotificationService() = default;
};

🔹 Step 2: Implement Concrete Services

// Low Level Module 1
class EmailService : public INotificationService {
public:
    void send(const string& message) override {
        cout << "Sending email: " << message << endl;
    }
};



// Low Level Module 2
class SMSService : public INotificationService {
public:
    void send(const string& message) override {
        cout << "Sending SMS: " << message << endl;
    }
};

🔹 Step 3: Update the High-Level Module to Use the Abstraction

// High-Level Module
class Notification {
    INotificationService* service;

public:
    Notification(INotificationService* s) : service(s) {}

    void notify(const string& message) {
        service->send(message);
    }
};

🔹 Step 4: Client Code

int main() {
    EmailService email;
    Notification n1(&email);
    n1.notify("Welcome to our service!");

    SMSService sms;
    Notification n2(&sms);
    n2.notify("Your OTP is 1234");

    return 0;
}

Benefits of Dependency Inversion

BenefitDescription
🔁 FlexibilitySwitch out implementations (Email ↔ SMS) without changing core logic
🧪 TestabilityEasily mock interfaces for unit testing
📦 ModularityEach component has a single responsibility and clear contract
🔧 MaintainabilityNew features can be added with minimal code changes
♻️ ReusabilityCore logic works with any class that implements the abstraction

📌 Recap: DIP in a Nutshell

  • 🔁 Invert dependencies to rely on interfaces, not concrete implementations.
  • ✅ Keep your high-level logic isolated from implementation details.
  • 🧪 Make unit testing and mocking painless.
  • 🔨 Architect software that is easy to change, extend, and maintain.

Note

dependency-inversion.jpg

DIP Violated

#include <iostream>
using namespace std;

class MySQLDatabase {  // Low-level module
public:
    void saveToSQL(string data) {
        cout << "Executing SQL Query: INSERT INTO users VALUES('" << data << "');" << endl;
    }
};

class MongoDBDatabase {  // Low-level module
public:
    void saveToMongo(string data) {
        cout << "Executing MongoDB Function: db.users.insert({name: '" << data << "'})" << endl;
    }
};

class UserService {  // High-level module (Tightly coupled)
private:
    MySQLDatabase sqlDb;  // Direct dependency on MySQL
    MongoDBDatabase mongoDb;  // Direct dependency on MongoDB

public:
    void storeUserToSQL(string user) {
        // MySQL-specific code
        sqlDb.saveToSQL(user);  
    }

    void storeUserToMongo(string user) {
        // MongoDB-specific code
        mongoDb.saveToMongo(user);  
    }
};

int main() {
    UserService service;
    service.storeUserToSQL("Aditya");
    service.storeUserToMongo("Rohit");
}

DIP Followed

#include <iostream>
using namespace std;

// Abstraction (Interface)
class Database {
public:
    virtual void save(string data) = 0; // Pure virtual function
};

// MySQL implementation (Low-level module)
class MySQLDatabase : public Database {
public:
    void save(string data) override {
        cout << "Executing SQL Query: INSERT INTO users VALUES('" << data << "');" << endl;
    }
};

// MongoDB implementation (Low-level module)
class MongoDBDatabase : public Database {
public:
    void save(string data) override {
        cout << "Executing MongoDB Function: db.users.insert({name: '" << data << "'})" << endl;
    }
};

// High-level module (Now loosely coupled)
class UserService {
private:
    Database* db;  // Dependency Injection

public:
    UserService(Database* database) {  
        db = database;
    }
    
    void storeUser(string user) {
        db->save(user);
    }
};

int main() {
    MySQLDatabase mysql;
    MongoDBDatabase mongodb;

    UserService service1(&mysql);
    service1.storeUser("Aditya");

    UserService service2(&mongodb);
    service2.storeUser("Rohit");
}