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 toKeyboard
.- If you want to replace
Keyboard
withTouchscreen
, you have to change theComputer
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
Concept | Explanation |
---|---|
High-Level Module | The main controller (e.g., Computer) |
Low-Level Module | The working components (e.g., Keyboard, Touchscreen) |
Abstraction | A shared interface both modules depend on (e.g., IInputDevice) |
Dependency Inversion | The 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
Principle | Full Form | Core Idea | High-Level Purpose | Real-World Analogy | Code Example Hint | Benefits | Violating It Leads To |
---|---|---|---|---|---|---|---|
S | Single Responsibility Principle | A class should have only one reason to change | Break down big classes into smaller ones, each doing one job | A chef shouldn’t also be the waiter, cashier, and cleaner | Split Invoice and InvoicePrinter | Maintainable, modular code | Hard to test, change, and reuse |
O | Open/Closed Principle | Software entities should be open for extension, but closed for modification | Add new functionality without changing existing code | Adding new plugins to a browser without editing its core | Use inheritance or strategy pattern | Flexible and extendable code | Risk of breaking existing functionality |
L | Liskov Substitution Principle | Subclasses should be replaceable for their parent classes without altering behavior | Design classes such that any subclass can be used safely in place of its base | A square should behave like a rectangle if inherited from it | Avoid incorrect inheritance like Bird -> Penguin flying | Polymorphic behavior works as expected | Unexpected behavior and bugs |
I | Interface Segregation Principle | Clients shouldn’t be forced to depend on interfaces they don’t use | Break big interfaces into smaller, specific ones | Don’t give a remote with 50 buttons to someone who only wants to change the volume | Split IMultifunctionPrinter into IPrint , IScan | Minimal and focused contracts | Confusing, bloated interfaces |
D | Dependency Inversion Principle | High-level modules should not depend on low-level modules, both should depend on abstractions | Use interfaces or abstract classes to decouple components | A plug point shouldn’t care which brand charger you use | Inject dependencies via constructor or interface | Flexible, testable, modular architecture | Tight 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)
- SRP gives each class one purpose.
- OCP lets you grow your app by adding features instead of modifying old code.
- LSP ensures that your new classes don’t break the old ones.
- ISP keeps your classes from knowing too much they don’t care about.
- DIP connects your parts flexibly through abstraction.
You can also Visit other tutorials of Embedded Prep
- What is eMMC (Embedded MultiMediaCard) memory ?
- Top 30+ I2C Interview Questions
- Bit Manipulation Interview Questions
- Structure and Union in c
- Little Endian vs. Big Endian: A Complete Guide
- Merge sort algorithm
Special thanks to @mr-raj for contributing to this article on Embedded Prep
Leave a Reply