Introdução: O que é Injeção de Dependência e para que serve?
No mundo do desenvolvimento de software, frequentemente nos deparamos com situações em que uma classe (ou módulo) depende de outra. Por exemplo, a classe UserService pode usar a classe DatabaseService para salvar dados. A abordagem tradicional é criar uma instância de DatabaseService diretamente dentro de UserService. Isso leva a um forte acoplamento do código, dificultando seu teste, manutenção e extensão.
Injeção de Dependência (DI) é um padrão de projeto que resolve esse problema. Em vez de criar dependências dentro da classe, elas são "injetadas" externamente. Isso implementa o princípio da Inversão de Controle (IoC): não é a classe que gerencia suas dependências, mas o código externo (um contêiner ou a parte chamadora) que as fornece.
Neste artigo, vamos explorar como implementar a Injeção de Dependência em TypeScript com exemplos simples. Você aprenderá por que DI é considerada uma boa prática e como ela ajuda a escrever código limpo e testável.
Problema: Acoplamento rígido sem DI
Vamos começar com um exemplo que mostra como não escrever código. Imagine que temos um aplicativo que envia notificações para usuários.
// Sem Injeção de Dependênciaclass EmailService { sendEmail(to: string, message: string): void { console.log(`Enviando email para ${to}: ${message}`); // ... lógica real de envio }}
class NotificationService { private emailService: EmailService;
constructor() { // Problema: acoplamento rígido this.emailService = new EmailService(); }
notifyUser(userEmail: string, message: string): void { this.emailService.sendEmail(userEmail, message); }}
// Usoconst notifier = new NotificationService();notifier.notifyUser('user@example.com', 'Hello!');Neste código, NotificationService está rigidamente acoplado a EmailService. E se quisermos enviar SMS ou notificações push? Teríamos que modificar o código de NotificationService. Além disso, testar NotificationService é difícil — não conseguimos substituir o EmailService real por um objeto mock.
Solução: Injeção de dependência via construtor
A maneira mais simples e comum de implementar DI é passar dependências através do construtor. Isso torna o código flexível e facilmente testável.
Primeiro, vamos definir uma interface para nosso serviço de notificações. Este é um passo fundamental: programamos no nível de abstrações, não de implementações concretas.
// Definindo uma interface para qualquer serviço de notificaçõesinterface INotificationService { send(to: string, message: string): void;}
// Implementação para Emailclass EmailService implements INotificationService { send(to: string, message: string): void { console.log(`[EMAIL] Enviado para ${to}: ${message}`); }}
// Implementação para SMSclass SMSService implements INotificationService { send(to: string, message: string): void { console.log(`[SMS] Enviado para ${to}: ${message}`); }}
// Classe que usa DIclass UserNotifier { private notifier: INotificationService;
// A dependência é injetada através do construtor constructor(notifier: INotificationService) { this.notifier = notifier; }
notifyUser(userContact: string, message: string): void { this.notifier.send(userContact, message); }}
// Usoconst emailNotifier = new UserNotifier(new EmailService());emailNotifier.notifyUser('alice@example.com', 'Seu pedido está pronto!');
const smsNotifier = new UserNotifier(new SMSService());smsNotifier.notifyUser('+7-999-123-45-67', 'Código de confirmação: 1234');Agora, UserNotifier não sabe qual serviço