The key is that the higher-level component shouldn't know anything about the lower-level component, and the lower-level component should be easily replaceable. This can be achieved by creating an abstract interface that describes the needed functionality and having the high-level modules depend on that interface. The concrete implementation of that functionality is then created separately and can be easily swapped out if needed.
Explain as if I am 10 years old
Imagine you're playing with building blocks and trying to build a castle. The castle is like a computer program, and the blocks are like the different modules of the program. You should build your castle by making the big blocks depend on a plan that tells them what shape the small blocks should be. If you build the tower of the castle using a lot of small blue blocks, and you decide later that you want to make it red, it will be very easy to change it because you need to change the small blue blocks to red ones, the tower will still be standing.
Loose Coupling, High Cohesion
"Loose coupling" and "high cohesion" are two related concepts in software development that refer to the relationship between different parts of a program.
"Coupling" refers to the degree to which different parts of a program depend on each other. "Loose coupling" means that different parts of the program have minimal dependencies on each other, which makes them more independent and easier to change. "High coupling" means that different parts of the program are heavily dependent on each other, making the program more difficult to change and maintain.
"Cohesion" refers to the degree to which the different parts of a program work together to achieve a single, well-defined purpose. "High cohesion" means that all the parts of a program work together logically to achieve a specific goal. "Low cohesion" means that the parts of the program don't work together well and may be doing unrelated things.
A loosely coupled, highly cohesive system is considered a good thing, as it gives you flexibility. By breaking the system into small, independent components with a single responsibility and working together to achieve a well-defined goal, you make it easier to understand, test, and maintain.
Violation
Violation of the Dependency Inversion Principle can occur when a high-level module (such as a class) depends directly on a low-level module (such as another class). A high-level module should depend on an abstraction rather than a concrete implementation.
Here is an example of a violation of the Dependency Inversion Principle in TypeScript:
class Engine {
start() {
console.log("Engine started!");
}
}
class Car {
private engine: Engine;
constructor() {
this.engine = new Engine();
}
start() {
this.engine.start();
}
}
In this example, the Car class depends directly on the Engine class. This creates a tight coupling between the two classes, making it difficult to change the implementation of one class without affecting the other. If we want to change the engine class with a new one, it will force us to change the Car class, which violates DIP.
Fix
To fix this violation and make the classes loosely coupled, we can use interfaces to define the required functionality and have the high-level module depend on the interface rather than the concrete implementation:
interface IEngine {
start(): void;
}
class Engine implements IEngine {
start() {
console.log("Engine started!");
}
}
class ElectricEngine implements IEngine {
start() {
console.log("Electric engine started!");
}
}
class Car {
private engine: IEngine;
constructor(engine: IEngine) {
this.engine = engine;
}
start() {
this.engine.start();
}
}
With this implementation, the Car class depends on the IEngine interface rather than the concrete Engine class. This means that the Car class can be used with any class that implements the IEngine interface, making it more flexible and less tightly coupled to the Engine class.
It's also worth noting that the implementation of the ElectricEngine class also implements the IEngine interface, which means that we could easily swap ElectricEngine for Engine in the Car class without changing any code inside the Car class. It makes the Car class less dependent on any specific engine implementation and more flexible to change.
Why DIP
Dependency Inversion Principle makes the software more flexible, easy to change, and easy to test. When different parts of a program depend on each other incorrectly, it can be difficult to change one part without breaking other parts. This is called tight coupling.
For example, imagine you have a program that's a bit like a cookbook with recipes for different meals. Now imagine that one of the recipes, like making a sandwich, depends on knowing the specific brand of bread you're using. If you want to change the bread, you must change the recipe.
But with the Dependency Inversion principle, you can build the program so that the recipe only depends on a more general concept, like bread. Then, you could change the bread brand without changing the recipe. It makes it a lot easier to change things, so you don't have to keep rewriting the whole program whenever you want to make a small change.
Another advantage is that when the components are loosely coupled, they are less dependent on each other and more reusable, which means that you could use them in other projects. And also, it will be easier to test your code because the dependencies between your classes and methods will be more explicit and easier to mock.