Dependency Injection Principle

↝ This principle states that a class should depend on abstractions, not on concretions. Once again, what do we mean by that definition? There are numerous concepts that we need to discuss, such as: What do we mean by dependency? Injection? Abstractions? and concretions? I believe we need to define these terms first before delving into the concept of the dependency injection principle.

Let's start by defining abstraction. It refers to defining what needs to be done without specifying the exact details of how to do it. This can be achieved using interfaces or abstract classes in other programming languages. The purpose of interfaces and abstract classes is to specify all the required fields, such as methods, without specifying how those methods are implemented.

Now, let's define concretion. It pertains to a specific instance of a class that implements the functionality outlined by the abstraction. So, to understand the definition of DIP, it means that we should depend on interfaces/abstract classes instead of concrete classes—the specific instance of a class. Why? Let's look at the TypeScript code below:

class OrderService {
  private database: MySQLDatabase; // Directly depends on a concrete database implementation

  constructor() {
    this.database = new MySQLDatabase(); // Creates a new MySQL database instance
  }

  saveOrder(order: Order) {
    this.database.save(order); // Calls the save method on the MySQL database
  }
}

Why does the code below violate the DIP? Because OrderService is tightly coupled to MySQLDatabase. What's wrong with that? If we need to use a different database, such as PostgreSQL or Firebase, we would have to modify the OrderService class again. Let's examine the code below to see how we can align it with the DIP.

interface Database {
  save(data: any): void;
}

class MySQLDatabase implements Database {
  save(data: any): void {
    console.log("Saving data to MySQL database");
  }
}

class PostgreSQLDatabase implements Database {
  save(data: any): void {
    console.log("Saving data to PostgreSQL database");
  }
}
class OrderService {
  private database: Database; // Depends on an abstraction (Database interface)

  constructor(database: Database) {
    // Accepts the database dependency through the constructor
    this.database = database;
  }

  saveOrder(order: Order) {
    this.database.save(order); // Calls the save method on the injected database
  }
}

const mysqlService = new OrderService(new MySQLDatabase());
mysqlService.saveOrder(new Order());

const postgresService = new OrderService(new PostgreSQLDatabase());
postgresService.saveOrder(new Order());

Now, the code above adheres to DIP because we define a Database interface that outlines the required functionality, and the OrderService class depends on this interface, making it flexible to work with any Database implementation. As a result, the OrderService class accepts the database dependency through its constructor, allowing us to inject different implementations depending on our needs.

To sum up, the dependency injection principle advocates for our code to depend on the general concept defined by the interface (abstraction) rather than on the specific code within a concrete class (instance). This enables us to easily swap between different concrete class instances, such as different database implementations, without affecting the rest of the code. Thank you and happy coding!