DEV Community

loading...

Inversão de dependências exemplificado

murilomaiaa profile image Murilo Maia Updated on ・4 min read

Quando começamos a programar é normal desenvolvermos programas simples. Quem nunca fez um cadastro de usuários com nome, email, senha e data de nascimento? Porém, conforme nossos os dias vão passando, nosso conhecimento técnico aumenta e nossos desafios também. E, do nada, esse cadastro de usuários agora cadastra também produtos, carrinhos e compras. Agora vamos imaginar que sempre que o usuário crie sua conta ou troque a senha ele receba um email. Você escolhe a maneira mais comum de enviar emails, através de SMTP. Porém, conforme seu sistema cresce você atinge o limite de emails e precisa trocar para um serviço especializado em email como o Amazon SES ou Sendgrid.
E aí? Você vai ter que refatorar todo o seu código para se adaptar a um mudança que deveria ser simples? Se você está começando, provavelmente sim. Porém existe uma maneira, muito fácil, de fazer com que essa mudança altere pouquíssimo código. Como já dizia Linus Torvalds

“Talk is cheap. Show me the code.”

ou em bom português,

“Falar é fácil. Me mostre o código”

Vou usar o Typescript como exemplo para esse artigo mas esse princípio é válido para qualquer linguagem com suporte a orientação a objetos.
Imagine que quando o usuário crie sua conta o sistema envie para ele um email de boas vindas. Esse e-mail vai ser enviado utilizando o Nodemailer. Então teremos:

Vale ressaltar que essas não são as melhores implementações possíveis, pois esse não é o foco do artigo.

//SendWelcomeEmailService.ts
import nodemailer from 'nodemailer'

export default class SendEmailToCreatedUserService {
  public async execute(email: string): Promise<void> {
   let transporter = nodemailer.createTransport({
      // nodemailer config
    });

    await transporter.sendMail({
      from: '"Murilo Maia" <murilomaia.bb@gmail.com>',
      to: email,
      subject: "Bem vindo",
      text: "Bem vindo a nossa aplicação",
      html: "<b>Bem vindo a nossa aplicação</b>",
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Mas, como citado no começo desse artigo, pode acontecer de você querer trocar o nodemailer para sendgrid. A implementação do service passaria a ser a seguinte.

//SendWelcomeEmailService.ts
import sendgrid from '@sendgrid/mail'

export default class SendEmailToCreatedUserService {
  public async execute(email: string): Promise<void> {
    sendgrid.setApiKey(process.env.SENDGRID_API_KEY)

    sendgrid.send({
      from: {
        name: 'Murilo Maia',
        email: 'murilomaia.bb@gmail.com'
      },
      to: email,
      subject: "Bem vindo",
      text: "Bem vindo a nossa aplicação",
      html: "<b>Bem vindo a nossa aplicação</b>",
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Até aí não tem tanto problema, agora imagina que você tem 3, 4 ou até 10 ocasiões que você tem envio de email. Você vai ter que refatorar todo o seu código. Pior ainda, imagine que, após pouco tempo você decida usar o Amazon SES? Você vai ter todo retrabalho de mudar essas funções. Você não vai querer isso, certo?
E é exatamente que o dependency inversion, ou inversão de dependência, resolve. O problema maior não é você trocar o serviço de envio de e-mails e sim depender de implementações concretas (biblioteca do Nodemailer, Sendgrid e Amazon SES) ao invés de uma abstração.
Voltando a frase do Linus Torvalds "Talk is cheap, show me the code"
Para começar vamos criar um interface que será a abstração de todos os serviços de email
vale ressaltar que gosto de isolar as regras de negócio em arquivos services e os serviços externos em providers

export default interface IMailProvider {
  // por fins didáticos diminuí o número de parâmetros para o mínimo possível
  sendMail: (to: string, subject: string, html: string) => Promise<void>
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos uma adaptação no service de envio de email

//SendWelcomeEmailService.ts
export default class SendEmailToCreatedUserService {
  private mailProvider: IMailProvider

  constructor(mailProvider: IMailProvider){
    this.mailProvider = mailProvider;
  }

  public async execute(email: string): Promise<void> {
    // validações
    await this.mailProvider.sendMail(
      to: email,
      subject: "Bem vindo",
      html: "<b>Bem vindo a nossa aplicação</b>",
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Note que agora o nosso service não depende de nenhuma implementação concreta e sim de uma abstração, a interface IMailProvider. Mas e aí? Nós temos a interface mas não temos nenhuma implementação para ela.
Primeiro vamos fazer a implementação do Nodemailer

import nodemailer, { Transporter } from "nodemailer";
import IMailProvider from "../model/IMailProvider";

// implementa nossa interface
export default class NodemailerMailProvider implements IMailProvider {
  private transporter: Transporter;

  constructor() {
    this.transporter = nodemailer.createTransport({
      // nodemailer config
    });
  }
  public async sendMail(to: string, subject: string, html: string) {
    await this.transporter.sendMail({
      from: '"Murilo Maia" <murilomaia.bb@gmail.com>',
      to,
      subject,
      html,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

E agora a nossa implementação do Sendgrid

import sendgrid from '@sendgrid/mail'

import IMailProvider from "../model/IMailProvider";

export default class SendgridMailProvider implements IMailProvider {
  constructor() {
    sendgrid.setApiKey(process.env.SENDGRID_API_KEY || '');
  }
  public async sendMail(to: string, subject: string, html: string): Promise<void> {
    sendgrid.send({
      from: '"Murilo Maia" <murilomaia.bb@gmail.com>',
      to,
      subject,
      html
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora quando formos instanciar nosso service não podemos fazer da seguinte maneira

const sendWelcomeMail = new SendWelcomeMailService()
Enter fullscreen mode Exit fullscreen mode

Se você fizer isso provavelmente sua IDE vai indicar um erro como "an argument for mail provider was not provided" ou "um argumento para mailProvider não foi informado". Isso acontece pq lá no construtor do service nós estamos esperando um mailProvider do tipo IMailProvider. Então nossa implementação vai ficar assim

const nodemailerMailProvider = new NodemailerMailProvider()
const sendWelcomeMail = new SendWelcomeEmailService(nodemailerMailProvider)
Enter fullscreen mode Exit fullscreen mode

ou

const sendgridMailProvider = new SendgridMailProvider()
const sendWelcomeMail = new SendWelcomeEmailService(sendgridMailProvider)
Enter fullscreen mode Exit fullscreen mode

Agora sempre que você quiser trocar o servico de envio de email é só voce criar uma classe que implementa a interface IMailProvider e na hora de instanciar os services que dependam dessa interface você passar o novo serviço.

Bônus

O que já estava bom, ainda dá pra melhorar. Imagine que você instancie esse service em mais de um lugar. Se toda vez que você for trocar o serviço de envio de email você tiver que instanciar tudo de novo vai dar um pouco de trabalho. Não é nada muito cansativo mas pode ser evitado utilizando fábricas.

// MakeSendWelcomeMailService.ts
export default function makeSendWelcomeMailService(): SendWelcomeMailService {
  const nodemailerMailProvider = new NodemailerMailProvider()
  const sendWelcomeMail = new SendWelcomeEmailService(nodemailerMailProvider)
  return sendWelcomeMail
} 
Enter fullscreen mode Exit fullscreen mode

Agora, sempre que você for instanciar o service você vai fazer

const sendWelcomeMail = makeSendWelcomeService()
Enter fullscreen mode Exit fullscreen mode

Assim, se você instancia seu service em mais de um lugar, basta trocar no makeSendWelcomMailService. Além disso, você desacopla ainda mais o seu código.

Discussion (0)

pic
Editor guide