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>",
});
}
}
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>",
})
}
}
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>
}
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>",
)
}
}
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,
});
}
}
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
})
}
}
Agora quando formos instanciar nosso service não podemos fazer da seguinte maneira
const sendWelcomeMail = new SendWelcomeMailService()
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)
ou
const sendgridMailProvider = new SendgridMailProvider()
const sendWelcomeMail = new SendWelcomeEmailService(sendgridMailProvider)
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
}
Agora, sempre que você for instanciar o service você vai fazer
const sendWelcomeMail = makeSendWelcomeService()
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.
Top comments (2)
Ótimo!
Continue postando sobre o assunto!
Eu gosto do assunto.
Mas OOP com class em JS me faz demorar mais pra ler os códigos Typescritp piora kkk
Tenho que ler mais.
Vai postar todos os princípios?
Agora sim!
Eu não usaria a tag #braziliandevs para textos em inglês!