O Princípio da Inversão de Dependência
Software muda. As coisas mudam o tempo todo. As bibliotecas ficam obsoletas e os frameworks são substituídos. Essas mudanças dão muito mais dor de cabeça quando nosso código está diretamento acoplado a esses tipos de ferramentas. O Princípio de Inversão de Dependência nos ajuda a reduzir essa dor.
Identificandos os problemas
O DIP pode ser resumido como "não dependa de coisas que podem mudar".
Seus componentes (classes, funções, etc.) não devem depender de outros componentes, mas sim de interfaces.
Olhe esse exemplo:
import { TwilioNotificationService } from 'src/infra/notification-service'
class PaymentService {
async pay (userId: string) {
// { ... logica de pagamento }
// chama um serviço externo
await TwilioNotificationService.sendPaymentReceivedNotification(userId)
}
}
// infra/notification-service.ts
class TwilioNotificationService {
async sendPaymentReceivedNotification(userId: string) {
// envia a notificação
}
}`
Como sabemos, o software é volátil. Depois de adicionar ao seu código essa chamada direta para a API do Twilio, você está assinando um contrato com eles que diz que você responderá a quaisquer alterações feitas em seus contratos. Por exemplo, se userId for alterado para um object em vez de uma string, você será responsável por alterar cada linha de código que chama essa API.
A equipe do Twilio não vai (e nem deve) alterar a API deles porque você precisa que ela seja uma string.
Refatorando
Vamos inverter essa dependência
Primeiro, devemos definir nosso contrato da maneira que precisamos que seja. No nosso caso um NotificationService que recebe uma string.
export interface PaymentNotificationService {
sendPaymentReceivedNotification: (userId: string) => Promise<{
ok: boolean
}>
}
Agora, ao invés de importar uma implementação concreta dentro do nosso serviço. Vamos recebê-lo do construtor da classe
import {
PaymentNotificationService
} from 'src/contracts/notification-service.ts'
class PaymentService {
private readonly notificationService: PaymentNotificationService
// a dependencia
constructor (notificationService: PaymentNotificationService) {
this.notificationService = notificationService
}
async pay (userId: string) {
// logica de pagamento
// chama o serviço da dependência
await this.notificationService.sendPaymentReceivedNotification(userId)
}
}
// contracts/notification-service.ts
export interface PaymentNotificationService {
sendPaymentReceivedNotification: (userId: string) => Promise<{
ok: boolean
}>
}
Agora nosso serviço não precisa saber como ou quem está implementando o serviço de notificação. Mas sim que ele não vai mudar. Não importa onde está sendo implementado, ele deve cumprir o contrato.
import { PaymentNotificationService } from 'src/contracts/notification-service'
// apenas pra fins didáticos, não é pra ser uma lib de vdd
import TwilioSDK from 'twilio-sdk'
export class TwilioNotificationService implements PaymentNotificationService {
async sendPaymentReceivedNotification (userId: string): Promise<{ ok: boolean }> {
const user = { userId: userId }
try {
const sendSuccess = await TwilioSDK.sendNotification('payment-received', user)
if (sendSuccess) return { ok: true }
return { ok: false }
} catch {
return { ok: false }
}
}
}
Usando-o a nosso favor, podemos substituir o serviço da Twilio por qualquer outra implementação deste contrato.
import TwilioNotificationService from './twilio'
import MailChimpNotificationService from './mailChimp'
import WhatsAppNotificationService from './whatsapp'
import PaymentService from './payment.ts'
const twilioService = new TwilioNotificationService()
const mailChimpService = new MailChimpNotificationService()
const whatsAppService = new WhatsAppNotificationService()
// todos são válidos
const PaymentServiceWithTwilio = new PaymentService(twilioService)
const PaymentServiceWithMailChimp = new PaymentService(mailChimpService)
const PaymentServiceWithWhatsApp = new PaymentService(whatsAppService)`
Quanto é suficiente?
Nas palavras do próprio Uncle Bob, o propósito por trás do princípio é bem simples: "não mencione o nome de nada que seja concreto e volátil".
A última palavra é muito importante para que a gente não entenda errado e comece a criar abstrações para classes default das linguagens como String ou Array.
Essas coisas não são voláteis, talvez alguns de seus componentes também não sejam. Cabe a você decidir se um componente é volátil ou não.
Você pode decidir inverter a dependência de uma classe que nunca mudará em toda a vida de seu aplicativo, mas o oposto também é válido. Pense nisso. Vale a pena a complexidade de criar uma interface e depois passar uma instância para o construtor da classe? Só você pode dizer.
Conclusão
Os princípios do SOLID são guidelines para ajudar nós devs a construirmos um software escalável. Guidelines, não regras. Use-as a seu favor, não contra você. A implementação de todos os princípios SOLID envolverá algum tipo de compensação entre complexidade e legibilidade ou testabilidade.
Nesse caso, estamos preparando nosso código para não sofrer alterações futuras na infraestrutura de envio de notificações. Para isso, nosso serviço não deve saber como esta notificação está sendo enviada na camada de infraestrutura. Isso é bom?
Neste caso, é. Mas pode não ser o seu caso. Use-o quando achar que faz sentido ocultar os detalhes em favor da flexibilidade.
Me acompanhe:
https://github.com/giovaniif
https://www.linkedin.com/in/giovani-ricco-farias/
https://giovaniif.medium.com
Referências:
Arquitetura Limpa - Robert C. Martin