DEV Community

Cover image for Dependency inversion explained
Murilo Maia
Murilo Maia

Posted on • Edited on

Dependency inversion explained

When we start to programming, it's usual to develop simple algorithms. Who has never created a CRUD of users? Althought, as time goes by, our knowledge increases and our challenges too.
Now, imagine that you are developing an application where everytime the user creates his account or forget his password, the system sends to him an email. As you are getting started, you choose to use the easiest and cheaper way to send emails: using SMTP.

I'm going to use Typescript in example but you can use any oriented object language.

As said before, our imaginary application will send email when the user register and forgot password. Our system will send an email with different subjects and different texts and html. So, we have these 2 services and nodemailer component.

// NodemailerMailProvider.ts
import nodemailer from 'nodemailer'

type SendMailParams = {
  to: string
  subject: string
  text: string
  html: string
}

export class NodemailerMailProvider {
  private transporter: nodemailer.Transporter;

  constructor(){
    this.transporter = nodemailer.createTransport(/* transport options */);
  }

  async sendMail({html,subject,text,to}: SendMailParams){
    let info = await this.transporter.sendMail({
      from: '"Our Application" <contact@ourapp.com>',
      to,
      subject,
      text,
      html,
    });

    console.log("Message sent: %s", info.messageId);
    console.log("Preview URL: %s", nodemailer.getTestMessageUrl(info));
  }
}
Enter fullscreen mode Exit fullscreen mode
//SendWelcomeEmailService.ts
import { NodemailerMailProvider } from "../providers";

export class SendWelcomeEmailService {
  private mailProvider: NodemailerMailProvider

  constructor(mailProvider: NodemailerMailProvider){
    this.mailProvider = mailProvider
  }

  public async execute(email: string, name: string): Promise<void> {
   await this.mailProvider.sendMail({
    to: email,
    subject: "Welcome",
    text: `Welcome to our application, ${name}!`,
    html: `<b>Welcome to our application, ${name}!</b>`,  
   })
  }
}
Enter fullscreen mode Exit fullscreen mode
// SendForgotEmailService.ts
import { NodemailerMailProvider } from "../providers/NodemailerMailProvider";

export class SendForgotEmailService {
  private mailProvider: NodemailerMailProvider

  constructor(mailProvider: NodemailerMailProvider){
    this.mailProvider = mailProvider
  }

  public async execute(email: string): Promise<void> {
   await this.mailProvider.sendMail({
    to: email,
    subject: "Password recovery",
    text: `A password recovery was requested`,
    html: `A <b>password recovery</b> was requested`,  
   })
  }
}
Enter fullscreen mode Exit fullscreen mode

But the system grows and starts to send a lot of emails. So, you decide to use Amazon SES. And now you have SESMailProvider

import nodemailer, { Transporter } from 'nodemailer';
import aws from 'aws-sdk';

type SendMailParams = {
  to: string
  subject: string
  text: string
  html: string
} 

export default class SESMailProvider  {
  private client: Transporter;

  constructor() {
    this.client = nodemailer.createTransport({
      SES: new aws.SES({
        apiVersion: '2010-12-01',
        region: 'us-east-1',
      }),
    });
  }

  public async sendMail({
    to,
    subject,
    html,
    text
  }: SendMailParams): Promise<void> {
    await this.client.sendMail({
      from: '"Our Application" <contact@ourapp.com>',
      to,
      subject,
      text,
      html
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

But now we have a problem, in the constructor of SendForgotEmailService and SendWelcomeEmailService we have this dependency mailProvider: NodemailerMailProvider. Now we have to substitute the type of mailProvider to SESMailProvider.

private mailProvider: NodemailerMailProvider

constructor(mailProvider: NodemailerMailProvider){
  this.mailProvider = mailProvider
}

// now is going to be 
private mailProvider: SESMailProvider

constructor(mailProvider: SESMailProvider){
  this.mailProvider = mailProvider
}
Enter fullscreen mode Exit fullscreen mode

This change affects only 2 services now, but imagine that our application sends email when user log in, when we want o send some notification as a like or a new post. As the system grows, we will have a lot of actions that send an email. So, if we want to change the MailProvider we will need to change in all constructors. That's not good. To solve it, you can create an interface and make the providers implement it.

// IMailProvider.ts
export type SendMailParams = {
  to: string
  subject: string
  text: string
  html: string
} 

export interface IMailProvider {
  sendMail(params: SendMailParams): Promise<void>
}
Enter fullscreen mode Exit fullscreen mode

Now, the providers are going to implement this interface. How both providers has the same method sendMail with the same signature the only change we need to do is to add implements IMailProvider

export class NodemailerMailProvider implements IMailProvider {}
export class SESMailProvider implements IMailProvider {}
Enter fullscreen mode Exit fullscreen mode

And after all the services will not recive NodemailerMailProvider, SESMailProvider or any other implementation. They will recive an IMailProvider.

private mailProvider: IMailProvider

constructor(mailProvider: IMailProvider){
  this.mailProvider = mailProvider
}
Enter fullscreen mode Exit fullscreen mode

Ok, now, we need to create the instance of the services. I'll create just for SendWelcomeEmailService to avoid repetition.

const sendWelcomeEmail = new SendWelcomeEmailService();
Enter fullscreen mode Exit fullscreen mode

This will result in a error because SendWelcomeEmailService recives an IMailProvider. But what is an IMailProvider? NodemailerMailProvider and SESMailProvider are IMailProvider beacuse both of them implements it. I am going to implement SendWelcomeEmailService injecting NodemailerMailProvider

const mailProvider = new NodemailerMailProvider();
const sendWelcomeEmail = new SendWelcomeEmailService(mailProvider);
Enter fullscreen mode Exit fullscreen mode

Now, if you want to change your mail provider the only thing you will need to do is change the mail provider you inject in your services and you won't need to change anything in the service.

But we still have a problem. The IMailProvider is required in more than one service. Then, we need to write const mailProvider = new NodemailerMailProvider() every time we need it. To solve it we can create factory methods. These methods are responsible to create the instances of our dependencies. The first factory we will create is makeIMailProvider that, obviously, will be responsible to create the a implementation of IMailProvider.

export function makeIMailProvider(): IMailProvider {
  return new NodemailerMailProvider()
}
Enter fullscreen mode Exit fullscreen mode

and the factories for the services

// look that the mail provider will be the same for both services and if we have another service needs IMailProvider it would use the same provider too
const mailProvider = makeIMailProvider();

export function makeSendWelcomeEmailService() {
  return new SendWelcomeEmailService(mailProvider)
}

export function makeSendForgotPasswordEmailService() {
  return new SendForgotPasswordEmailService(mailProvider)
}
Enter fullscreen mode Exit fullscreen mode

When you need the services you can just

const sendWelcomeEmail =  makeSendWelcomeEmailService();
// or
const sendForgotPasswordEmail = makeSendForgotPasswordEmailService();
Enter fullscreen mode Exit fullscreen mode

And the best part, if you want to change the implementation of IMailProvider you only need to change the factory method

export function makeIMailProvider(): IMailProvider {
  // return new SendgridMailProvider()
  // return new MailchimpMailProvider()
  return new SESMailProvider()
}
Enter fullscreen mode Exit fullscreen mode

Note that the provider returned in the factory must implement the inerface IMailProvider.

Robert Matin in his book clean arquitecture says

The Dependency Inversion Principle (DIP) tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.

If you want to see the full code check the repository in https://github.com/murilomaiaa/dependency-inversion

Top comments (1)

Collapse
 
urielsouza29 profile image
Uriel dos Santos Souza

Usar #braziliandevs escrever em inglês?