DEV Community

Cover image for Dependency Inversion Principle (DIP) in Typescript
Hasan Zohdy
Hasan Zohdy

Posted on

Dependency Inversion Principle (DIP) in Typescript

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.

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

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

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.

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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

import PaymentMethod from './payment-method';

class Cart {
  public constructor(private payment: PaymentMethod) {
    // 
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

const cart = new Cart(new CreditCardPayment());
Enter fullscreen mode Exit fullscreen mode

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

class Cart {
  public constructor(private payment: PaymentMethod) {
    // 
  }
}
Enter fullscreen mode Exit fullscreen mode

Setter

class Cart {
  private payment: PaymentMethod;
  public setPayment(payment: PaymentMethod): void {
    this.payment = payment;
  }
}
Enter fullscreen mode Exit fullscreen mode

Method

class Cart {
  public pay(payment: PaymentMethod): void {
    payment.pay();
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)