Dependency Inversion Principle (DIP) in Typescript
Tags: typescript, oop
Published: 2023-10-18 09:58:44


## Introduction

Dependency Inversion Principle (DIP) is the last principle in `S.O.L.I.D` principles.

The principle states that: a high level code should depend on abstractions, not on concretions.


- High-level modules contain application-specific business logic. They are responsible for controlling and orchestrating the application's behavior.
- Low-level modules deal with implementation details and services that the high-level modules use. They handle tasks like data access, I/O operations, and external service integration.

`DIP` encourages the use of abstract classes or interfaces to define the contracts between high-level and low-level modules. High-level modules depend on abstractions (interfaces or abstract classes) rather than concrete implementations. This allows for flexibility and the ability to swap out implementations without modifying high-level code.

## What is an abstraction

An abstraction is a way to hide the implementation details of a class and show only the functionalities to the users either by using an interface or an abstract class.

## What is a concretion

A concrete class is a class that implements an abstraction.

So we can conclude that an interface is called abstraction, a class that implements an interface is called concrete class.

## Why we need to depend on abstractions

In nutshell, we need to decuple the high level code and level code from concrete classes, why would we do that?

Well, to make the code more flexible and easy to maintain.

## How Dependency Inversion makes the code flexible

Let's take an example, we'are developing an e-commerce application, and we have a cart class that needs a payment method to process the payment.

Let's say we will use a `CreditCardPayment` class.

```ts
import CreditCardPayment from './payments/credit-card-payment';

class Cart {
  private payment: CreditCardPayment;
  public constructor() {
    this.payment = new CreditCardPayment();
  }
  // ...rest of the code
}
```

This will work normally, but what if we decided to add another payment method, let's say `PayPalPayment` class.

In this case we'll have to update any code that creates the credit card payment class and replace it with paypal payment class.

## How to solve this problem

We can solve this problem by depending on an abstraction instead of a concrete class.

```ts
interface PaymentMethod {
  pay(): void;
}

class CreditCardPayment implements PaymentMethod {
  public pay(): void {
    console.log('Paying with credit card');
  }
}

class PayPalPayment implements PaymentMethod {
  public pay(): void {
    console.log('Paying with paypal');
  }
}
```

Now we can depend on the `PaymentMethod` interface instead of the concrete classes.

```ts
import PaymentMethod from './payment-method';

class Cart {
  public constructor(private payment: PaymentMethod) {
    // 
  }
}
```

Now we can pass any class that implements the `PaymentMethod` interface.

```ts
import Cart from './cart';
import CreditCardPayment from './payments/credit-card-payment';

const cart = new Cart(new CreditCardPayment());
```

Here we made two things: 

1. We decoupled the high level code from the low level code by depending on an abstraction instead of a concrete class.
2. We passed created a new instance of payment outside the cart class and passed it to the class, this is also called `dependency injection` and makes it `Decoupled`

## What is dependency injection

Dependency Injection is a design pattern that is primarily a way to implement `Dependency Inversion Principle`.

The idea is simple, instead of creating the dependencies inside the class, we create them outside the class and pass them to the class.

## Why we need to use dependency injection

For many reasons actually:

- `Decoupling` the high level code from the low level code.
- `Testing` the class will be easier, we can pass a mock class instead of the real class.
- `Reusability` we can reuse the class with different dependencies.
- `Flexibility` we can change the dependencies without changing the class.

## How to implement dependency injection

There are many ways to implement dependency injection, we can use a `constructor`, a `setter` or a `method`.

### Constructor

```ts
class Cart {
  public constructor(private payment: PaymentMethod) {
    // 
  }
}
```

### Setter

```ts
class Cart {
  private payment: PaymentMethod;
  public setPayment(payment: PaymentMethod): void {
    this.payment = payment;
  }
}
```

### Method

```ts
class Cart {
  public pay(payment: PaymentMethod): void {
    payment.pay();
  }
}
```

Later on, we'll take more in depth look at dependency injection with Inversion Of Control (IoC) containers.

## Conclusion

Dependency Inversion Principle (DIP) is the last principle in `S.O.L.I.D` principles.

The principle states that: a high level code should depend on abstractions, not on concretions.

We can solve this problem by depending on an abstraction instead of a concrete class.


## Following the list

You can see the updated list of design principles from the following link
https://mentoor.io/en/posts/634524154/open-closed-principle-in-typescript

Join us in our Discord Community
https://discord.gg/XDZcTuU8c8