D – Dependency Inversion Principle (DIP) | Master CPP Design Patterns 2025

Definition (Simple Terms)

Dependency Inversion Principle : “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.”

Let’s Break It Down

This sounds complex, right? Let’s simplify.

Imagine you’re building a robot. The brain of the robot is the high-level module — it decides what to do. The legs or arms are low-level modules — they do the actual work.

Now, if the robot’s brain is tightly connected (depends directly) to the specific type of leg motors, then anytime you change the leg motors, you also have to change the brain’s code. That’s bad.

DIP says: “Don’t connect the brain directly to the legs. Let both talk through a common language (interface or abstract class).”

That way, if you ever replace the legs with wheels, the brain doesn’t need to know — it just sends the same instructions.

Goal of DIP

  • Reduce tight coupling between classes/modules.
  • Increase flexibility, maintainability, and testability of code.
  • Make it easier to swap or change parts of the system.

Traditional (Wrong) Design Without DIP

class Keyboard {
public:
    void input() {
        std::cout << "Keyboard input received\n";
    }
};

class Computer {
    Keyboard keyboard;  // Direct dependency
public:
    void getInput() {
        keyboard.input();
    }
};

What’s wrong?

  • Computer is tightly bound to Keyboard.
  • If you want to replace Keyboard with Touchscreen, you have to change the Computer class.

DIP Applied (Right Way)

Step 1: Create an abstraction (interface)

class IInputDevice {
public:
    virtual void input() = 0; // Pure virtual function
    virtual ~IInputDevice() = default;
};

Step 2: Implement the abstraction in concrete classes

class Keyboard : public IInputDevice {
public:
    void input() override {
        std::cout << "Keyboard input received\n";
    }
};

class Touchscreen : public IInputDevice {
public:
    void input() override {
        std::cout << "Touchscreen input received\n";
    }
};

Step 3: Depend on the abstraction

class Computer {
    IInputDevice* inputDevice; // Depends on abstraction
public:
    Computer(IInputDevice* device) : inputDevice(device) {}

    void getInput() {
        inputDevice->input();  // Calls via interface
    }
};

Step 4: Use it flexibly

int main() {
    Keyboard kb;
    Touchscreen ts;

    Computer comp1(&kb);  // Works with keyboard
    Computer comp2(&ts);  // Works with touchscreen

    comp1.getInput();  // Output: Keyboard input received
    comp2.getInput();  // Output: Touchscreen input received

    return 0;
}

Key Takeaways

ConceptExplanation
High-Level ModuleThe main controller (e.g., Computer)
Low-Level ModuleThe working components (e.g., Keyboard, Touchscreen)
AbstractionA shared interface both modules depend on (e.g., IInputDevice)
Dependency InversionThe direction of dependency is flipped — both rely on abstraction

Benefits of Using DIP

  • ✅ Easy to switch or add new modules (like adding a mouse or joystick).
  • ✅ Improved testability (you can inject mock devices for testing).
  • ✅ Better code organization and readability.
  • ✅ Promotes Open/Closed Principle (open to extension, closed to modification).

Real-World Analogy

Think of a power plug. Your phone charger (low-level device) and your home socket (high-level supply) both depend on a standard plug shape (interface). If tomorrow you buy a new charger, as long as it supports the same plug, you don’t need to rewire your home.

DIP and Unit Testing

Since you depend on interfaces, you can write mock versions during testing:

class MockInputDevice : public IInputDevice {
public:
    void input() override {
        std::cout << "Mock input for testing\n";
    }
};

Final Thoughts

  • DIP doesn’t mean no dependencies. It means depend on abstractions rather than concrete implementations.
  • It’s not about eliminating dependencies, but inverting the direction of dependency to favor abstraction.

Full Comparison of SOLID Principles

PrincipleFull FormCore IdeaHigh-Level PurposeReal-World AnalogyCode Example HintBenefitsViolating It Leads To
SSingle Responsibility PrincipleA class should have only one reason to changeBreak down big classes into smaller ones, each doing one jobA chef shouldn’t also be the waiter, cashier, and cleanerSplit Invoice and InvoicePrinterMaintainable, modular codeHard to test, change, and reuse
OOpen/Closed PrincipleSoftware entities should be open for extension, but closed for modificationAdd new functionality without changing existing codeAdding new plugins to a browser without editing its coreUse inheritance or strategy patternFlexible and extendable codeRisk of breaking existing functionality
LLiskov Substitution PrincipleSubclasses should be replaceable for their parent classes without altering behaviorDesign classes such that any subclass can be used safely in place of its baseA square should behave like a rectangle if inherited from itAvoid incorrect inheritance like Bird -> Penguin flyingPolymorphic behavior works as expectedUnexpected behavior and bugs
IInterface Segregation PrincipleClients shouldn’t be forced to depend on interfaces they don’t useBreak big interfaces into smaller, specific onesDon’t give a remote with 50 buttons to someone who only wants to change the volumeSplit IMultifunctionPrinter into IPrint, IScanMinimal and focused contractsConfusing, bloated interfaces
DDependency Inversion PrincipleHigh-level modules should not depend on low-level modules, both should depend on abstractionsUse interfaces or abstract classes to decouple componentsA plug point shouldn’t care which brand charger you useInject dependencies via constructor or interfaceFlexible, testable, modular architectureTight coupling, hard to replace components

Detailed Descriptions Per Column

Core Idea

  • S: One job per class.
  • O: Add, don’t change.
  • L: Substitutable behavior.
  • I: Small, focused interfaces.
  • D: Depend on interfaces, not concrete classes.

Real-World Analogy

  • Each principle is inspired by common-sense organization and responsibility. These analogies help you remember and visualize them better.

Code Hint

  • Gives a small direction of what to implement or avoid in code for each principle.

Benefits

  • Each principle improves maintainability, testability, and flexibility in different ways. Together, they lead to a clean and scalable architecture.

Violations Lead To

  • Points out what goes wrong when the principle is not followed — like tight coupling, difficult changes, or confusing code.

How They Work Together (Flow Summary)

  1. SRP gives each class one purpose.
  2. OCP lets you grow your app by adding features instead of modifying old code.
  3. LSP ensures that your new classes don’t break the old ones.
  4. ISP keeps your classes from knowing too much they don’t care about.
  5. DIP connects your parts flexibly through abstraction.

You can also Visit other tutorials of Embedded Prep 

Special thanks to @mr-raj for contributing to this article on Embedded Prep

Leave a Reply

Your email address will not be published. Required fields are marked *