Published on

[SOLID] Dependencies Inversion Princple

Authors
  • avatar
    Name
    Khánh
    Twitter

When we use

Statement

the dependency inversion principle is a specific methodology for loosely coupled software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details. The principle states:

  • High-level modules should not import anything from low-level modules. Both should depend on abstractions
  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

In this context, a module can be understood as a class. When writing a high-level module, it should not directly depend on a low-level module. For example, in the relationship between services and repositories, the services class should depend on an interface, and repositories should implement that interface.

Example

// Khởi tạo interface chung cho toàn bộ database
interface IDatabase {
  connect: () => any;
  getById: (recordId: string) => any;
  update: (recordId: string, data: any) => any;
}

// khởi tạo low-level interface
interface IMongoDatabase extends IDatabase {}

interface IPostgresDatabase extends IDatabase {}

// triển khai low-level class cho Mongo
class Mongo implements IMongoDatabase {
  public connect() {
    console.log("mongodb connected");
  }
  getById(recordId: string) {
    console.log("mongodb get record: ", recordId);
  }
  update(recordId: string, data: any) {
    console.log("mongodb update record: ", recordId, " with data: ", data);
  }
}

// triển khai low-level class cho Postgres
class Postgres implements IPostgresDatabase {
  public connect() {
    console.log("postgres connected");
  }
  getById(recordId: string) {
    console.log("postgres get record: ", recordId);
  }
  update(recordId: string, data: any) {
    console.log("postgres update record: ", recordId, " with data: ", data);
  }
}

// Khởi tạo high-level class 
// Ở đây sử dụng thằng Car để chỉ ra rằng các function của nó 
// không bị phụ thuộc vào các low-level class mà 
// nó bị phụ thuộc vào interface
class Car {
  private db: IDatabase;
  constructor(db: IDatabase) {
    this.db = db;
    this.db.connect();
  }
  public get() {
    return this.db.getById("carId");
  }
  public changeDb(db: IDatabase){
    this.db = db;
    this.db.connect()
  }
}

const mongoDb = new Mongo();
const car = new Car(mongoDb);
car.get();
// "mongodb connected"
// "mongodb get record: ",  "carId"

const postgresDb = new Postgres();
car.changeDb(postgresDb);
car.get()
// "postgres connected"
// "postgres get record: ",  "carId"

Conclusion

Using Dependency Inversion Principle (DIP) makes the code more flexible when it comes to changes. However, for interfaces that only have one implementation, DIP may not provide significant benefits and can add complexity to the code.

Advantages:

  • Reduces coupling between modules.
  • Makes code easier to maintain and change modules.
  • Facilitates writing unit tests (due to easy module changes).

Disadvantages:

  • Dependency Injection (DI) concepts can be challenging for new developers to grasp.
  • Using interfaces can make debugging difficult because it's not always clear which class is being used.
  • Increases code complexity.

Overall, while DIP can enhance flexibility and maintainability, it's important to consider its applicability and potential drawbacks, especially in scenarios with single implementation interfaces.

References