DEV Community

Cover image for DEPENDENCY INVERSION PRINCIPLE
Victor Lis Bronzo
Victor Lis Bronzo

Posted on

DEPENDENCY INVERSION PRINCIPLE

High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces).

In practice, your business rule shouldn't know if you use MySQL, Stripe, or AWS. It should depend only on a contract (Interface) that says WHAT needs to be done, and not HOW it will be done.


RIGID IMPLEMENTATION

The most common mistake on a daily basis is calling a new ExternalTool() directly inside your use case.

When you do this, your code depends on the tool.

If the tool is discontinued or the API changes, you will have to open the "heart" of your system to fix it.


A BAD EXAMPLE

The business class is tightly coupled to the email provider SendGrid. This is a bad practice.

Code Example:

import { SendGridProvider } from 'sendgrid-sdk';

// BAD: The service instantiates the tool directly.
class RegisterUserUseCase {
  private mailProvider: SendGridProvider;

  constructor() {
    // Tight coupling!
    this.mailProvider = new SendGridProvider('API_KEY');
  }

  public async execute(email: string): Promise<void> {
    // ...complex business logic...

    // If SendGrid changes or goes down, this class breaks.
    await this.mailProvider.sendEmail(email, "Welcome!");
  }
}
Enter fullscreen mode Exit fullscreen mode

TURNING THE TABLES

The solution is not to depend on the concrete class SendGridProvider, but rather to create a generic interface within our own domain.

Our service now receives this dependency ready via the constructor (Dependency Injection).

The most interesting part is that our application doesn't need to know exactly who is under the hood, whether it's Resend, EmailJS, or another, it just follows the template.


A GOOD EXAMPLE

The service now depends only on the contract. The infrastructure must obey.

Code Example:

// GOOD: The contract belongs to OUR application.
interface IMailProvider {
  send(to: string, message: string): Promise<void>;
}

class RegisterUserUseCase {
  // We receive the abstraction from the outside (Dependency Injection)
  constructor(private mailProvider: IMailProvider) {}

  public async execute(email: string): Promise<void> {
    // ...complex business logic...

    // The service has no idea which tool is sending it
    await this.mailProvider.send(email, "Welcome!");
  }
}
Enter fullscreen mode Exit fullscreen mode

THE POWER OF INVERSION

The biggest advantage of DIP is Modularity.

If tomorrow your client asks to swap SendGrid for AWS SES, you just create a new implementation of the interface. The RegisterUserUseCase class suffers no changes.

Besides that, Unit Tests become absurdly easier, because you can inject a fake email Mock without needing to trigger real emails.


My Links
Github: victor-lis-bronzo
Linkedin: victor-lis-bronzo
Portfolio: portfolio.victorlisbronzo.me
Coolest Portfolio: victorlisbronzo.me


Leave your reaction ❤️
Were you already applying Dependency Injection before knowing it was the basis of DIP?

Top comments (1)

Collapse
 
algorhymer profile image
sassenheimer

Don't buy the dip, kid.