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 everytim…


This content originally appeared on DEV Community and was authored by Murilo Maia

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));
  }
}
//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>`,  
   })
  }
}
// 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`,  
   })
  }
}

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
    });
  }
}

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
}

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>
}

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 {}

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
}

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

const sendWelcomeEmail = new SendWelcomeEmailService();

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);

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()
}

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)
}

When you need the services you can just

const sendWelcomeEmail =  makeSendWelcomeEmailService();
// or
const sendForgotPasswordEmail = makeSendForgotPasswordEmailService();

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()
}

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


This content originally appeared on DEV Community and was authored by Murilo Maia


Print Share Comment Cite Upload Translate Updates
APA

Murilo Maia | Sciencx (2021-11-01T23:02:22+00:00) Dependency inversion explained. Retrieved from https://www.scien.cx/2021/11/01/dependency-inversion-explained/

MLA
" » Dependency inversion explained." Murilo Maia | Sciencx - Monday November 1, 2021, https://www.scien.cx/2021/11/01/dependency-inversion-explained/
HARVARD
Murilo Maia | Sciencx Monday November 1, 2021 » Dependency inversion explained., viewed ,<https://www.scien.cx/2021/11/01/dependency-inversion-explained/>
VANCOUVER
Murilo Maia | Sciencx - » Dependency inversion explained. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/11/01/dependency-inversion-explained/
CHICAGO
" » Dependency inversion explained." Murilo Maia | Sciencx - Accessed . https://www.scien.cx/2021/11/01/dependency-inversion-explained/
IEEE
" » Dependency inversion explained." Murilo Maia | Sciencx [Online]. Available: https://www.scien.cx/2021/11/01/dependency-inversion-explained/. [Accessed: ]
rf:citation
» Dependency inversion explained | Murilo Maia | Sciencx | https://www.scien.cx/2021/11/01/dependency-inversion-explained/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.