DEV Community

Cover image for SOLID: D - Dependency Inversion Principle (DIP)
Paulo Messias
Paulo Messias

Posted on

SOLID: D - Dependency Inversion Principle (DIP)

Introduction to DIP:
The Dependency Inversion Principle (DIP) is the final principle in the SOLID design principles. DIP states that high-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces). Additionally, abstractions should not depend on details; details should depend on abstractions. This principle is essential for creating systems that are modular, flexible, and easy to maintain.

Objectives of DIP:

  • Promote Loose Coupling: Reduces the dependencies between high-level and low-level modules, making the system more flexible.
  • Enhance Modularity: Encourages the use of interfaces and abstractions, which facilitates the replacement of components without affecting the entire system.
  • Improve Testability: By depending on abstractions, modules become easier to mock or stub during testing.
  • Support Reusability: Modules that depend on abstractions can be reused in different contexts without modification.

Bad Practice Example (Classes):
Here we have a LightBulb class that directly depends on a Switch class, creating tight coupling.

class LightBulb {
  turnOn(): void {
    console.log("LightBulb is turned on");
  }

  turnOff(): void {
    console.log("LightBulb is turned off");
  }
}

class Switch {
  lightBulb: LightBulb;

  constructor(lightBulb: LightBulb) {
    this.lightBulb = lightBulb;
  }

  operate(): void {
    this.lightBulb.turnOn();
  }
}

const lightBulb = new LightBulb();
const mySwitch = new Switch(lightBulb);
mySwitch.operate();
Enter fullscreen mode Exit fullscreen mode

In this approach, the Switch class is tightly coupled to the LightBulb class, violating DIP.

Good Practice Example (Classes):
To follow DIP, we can introduce an abstraction (e.g., Switchable) that both classes depend on.

interface Switchable {
  turnOn(): void;
  turnOff(): void;
}

class LightBulb implements Switchable {
  turnOn(): void {
    console.log("LightBulb is turned on");
  }

  turnOff(): void {
    console.log("LightBulb is turned off");
  }
}

class Switch {
  device: Switchable;

  constructor(device: Switchable) {
    this.device = device;
  }

  operate(): void {
    this.device.turnOn();
  }
}

const lightBulb = new LightBulb();
const mySwitch = new Switch(lightBulb);
mySwitch.operate();
Enter fullscreen mode Exit fullscreen mode

In this approach, the Switch class depends on the Switchable interface, making it independent of the concrete LightBulb implementation.

Bad Practice Example (Functions):
Here’s an example where a function directly depends on a low-level module.

class DatabaseConnection {
  connect(): void {
    console.log("Connected to the database");
  }
}

class UserService {
  database: DatabaseConnection;

  constructor() {
    this.database = new DatabaseConnection();
  }

  saveUser(): void {
    this.database.connect();
    console.log("User saved");
  }
}
Enter fullscreen mode Exit fullscreen mode

In this approach, the UserService class is directly dependent on the DatabaseConnection class, violating DIP.

Good Practice Example (Functions):
To follow DIP, we can introduce an abstraction that the UserService class depends on.

interface Database {
  connect(): void;
}

class DatabaseConnection implements Database {
  connect(): void {
    console.log("Connected to the database");
  }
}

class UserService {
  database: Database;

  constructor(database: Database) {
    this.database = database;
  }

  saveUser(): void {
    this.database.connect();
    console.log("User saved");
  }
}

const databaseConnection = new DatabaseConnection();
const userService = new UserService(databaseConnection);
userService.saveUser();
Enter fullscreen mode Exit fullscreen mode

In this approach, the UserService class depends on the Database interface, making it more flexible and testable.

Conclusion:
The Dependency Inversion Principle is crucial for creating flexible and maintainable systems. By ensuring that high-level modules do not directly depend on low-level modules, but instead rely on abstractions, we can create systems that are more modular, easier to test, and more resilient to change.

Top comments (0)