Dependency inversion principle#

DIP

tl;dr: High-level modules should not depend on low-level modules. Both should depend on abstractions.

The Dependency inversion principle (DIP) is a fundamental principle in object-oriented programming that aims to reduce dependencies between modules or classes by inverting the traditional flow of control.

In simpler terms, the DIP suggests that high-level modules or classes should not depend on low-level modules directly. Instead, both should depend on abstractions or interfaces. This principle promotes loose coupling, flexibility, and modularity in software design.

To explain it further, let’s consider a real-world example. Imagine you have a class called Car that needs to interact with a class called Engine to perform various operations. In a naive approach, the Car class directly depends on the Engine class. However, this creates a tight coupling between the two classes, making it difficult to change or extend the system.

Violation of the DIP (Java)#
 class Engine {
     public void start() {
         // Engine start logic
     }
 }

 class Car {
     private Engine engine;

     public Car() {
         this.engine = new Engine();
     }

     public void startCar() {
         engine.start();
         // Other car startup logic
     }
 }

In this example, the Car class directly depends on the concrete Engine class. This tightly couples the Car class to the specific implementation of the Engine class. As a result, it becomes challenging to replace the Engine with a different implementation or to test the Car class independently.

To correct the violation of the DIP, we can introduce an abstraction or interface that both the Car and Engine classes can depend on.

Corrected example of the DIP (Java)#
 interface Engine {
     void start();
 }

 class GasolineEngine implements Engine {
     public void start() {
         // Gasoline engine start logic
     }
 }

 class ElectricEngine implements Engine {
     public void start() {
         // Electric engine start logic
     }
 }

 class Car {
     private Engine engine;

     public Car(Engine engine) {
         this.engine = engine;
     }

     public void startCar() {
         engine.start();
         // Other car startup logic
     }
 }

In this corrected example, we introduced the Engine interface, which defines the start() method. The GasolineEngine and ElectricEngine classes implement this interface with their specific implementations.

The Car class no longer depends on the concrete Engine class but instead depends on the abstraction provided by the Engine interface. This promotes loose coupling, as the Car class can now work with any implementation of the Engine interface without being aware of the specific details.

The advantage of applying the DIP is that it allows for flexibility, modularity, and easier testing. You can easily introduce new engine implementations or swap them out without modifying the Car class, as long as they adhere to the Engine interface contract. It also enables easier unit testing by mocking or substituting the Engine interface with test doubles.

By inverting the dependencies and depending on abstractions rather than concrete implementations, we achieve better decoupling, maintainability, and extensibility in our code.