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
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/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.