<h2>Compreendendo Inversão de Dependência</h2>
<p>A inversão de dependência é um princípio fundamental em arquitetura de software que inverte a forma como os módulos se relacionam. Em vez de classes de alto nível dependerem diretamente de classes de baixo nível, ambas devem depender de abstrações. Isso reduz acoplamento, facilita testes e torna o código mais flexível e sustentável.</p>
<p>Sem inversão de dependência, uma classe que gerencia usuários precisaria instanciar diretamente um banco de dados específico, criando um vínculo forte. Se você precisasse trocar de banco de dados, teria que modificar a classe de usuários. Com inversão de dependência, a classe depende de uma interface, e qualquer implementação dessa interface pode ser injetada.</p>
<h2>Diferenças Práticas Entre tsyringe e InversifyJS</h2>
<h3>tsyringe: Simplicidade e Decoradores</h3>
<p>O <strong>tsyringe</strong> é uma biblioteca leve de injeção de dependência criada pela Microsoft. Ele usa decoradores TypeScript de forma intuitiva e requer menos configuração inicial. É ideal para projetos que valorizam simplicidade sem sacrificar funcionalidades essenciais.</p>
<pre><code class="language-typescript">import { injectable, inject, container } from 'tsyringe';
// Definir uma abstração
interface DatabaseConnection {
connect(): Promise<void>;
query(sql: string): Promise<any[]>;
}
// Implementação concreta
@injectable()
class PostgresConnection implements DatabaseConnection {
async connect(): Promise<void> {
console.log('Conectando ao PostgreSQL...');
}
async query(sql: string): Promise<any[]> {
return [{ id: 1, name: 'João' }];
}
}
// Serviço que depende da abstração
@injectable()
class UserRepository {
constructor(@inject('DatabaseConnection') private db: DatabaseConnection) {}
async getUsers() {
await this.db.connect();
return this.db.query('SELECT * FROM users');
}
}
// Registrar no container
container.register<DatabaseConnection>('DatabaseConnection', {
useClass: PostgresConnection,
});
container.registerSingleton(UserRepository);
// Usar
const userRepo = container.resolve(UserRepository);
userRepo.getUsers().then(console.log);</code></pre>
<p>O tsyringe é direto: você decora classes com <code>@injectable()</code>, registra no container e resolve quando necessário. A curva de aprendizado é rápida, e é excelente para aplicações de pequeno a médio porte.</p>
<h3>InversifyJS: Flexibilidade e Controle Avançado</h3>
<p>O <strong>InversifyJS</strong> é mais robusto e oferece controle fino sobre a injeção de dependências. Ele usa identificadores simbólicos e permite configurações mais complexas. É a escolha para projetos grandes com requisitos avançados de DI.</p>
<pre><code class="language-typescript">import { Container, injectable, inject } from 'inversify';
// Definir símbolos para melhor tipagem
const TYPES = {
DatabaseConnection: Symbol.for('DatabaseConnection'),
UserRepository: Symbol.for('UserRepository'),
};
// Interface
interface DatabaseConnection {
connect(): Promise<void>;
query(sql: string): Promise<any[]>;
}
// Implementação
@injectable()
class PostgresConnection implements DatabaseConnection {
async connect(): Promise<void> {
console.log('Conectando ao PostgreSQL...');
}
async query(sql: string): Promise<any[]> {
return [{ id: 1, name: 'Maria' }];
}
}
// Repositório
@injectable()
class UserRepository {
constructor(
@inject(TYPES.DatabaseConnection)
private db: DatabaseConnection
) {}
async getUsers() {
await this.db.connect();
return this.db.query('SELECT * FROM users');
}
}
// Configurar container
const container = new Container();
container.bind<DatabaseConnection>(TYPES.DatabaseConnection)
.to(PostgresConnection)
.inSingletonScope();
container.bind<UserRepository>(TYPES.UserRepository).to(UserRepository);
// Usar
const userRepo = container.get<UserRepository>(TYPES.UserRepository);
userRepo.getUsers().then(console.log);</code></pre>
<p>A principal vantagem do InversifyJS é o uso de símbolos, que previne colisões de nomes e oferece melhor segurança de tipo. Escopos avançados (singleton, transient, request) e suporte a factories complexas o tornam mais poderoso.</p>
<h2>Casos de Uso Reais e Implementação</h2>
<h3>Exemplo: Sistema de Autenticação com tsyringe</h3>
<p>Considere um aplicativo onde você precisa suportar múltiplas estratégias de autenticação. Com inversão de dependência, você alterna estratégias sem modificar o código que as utiliza.</p>
<pre><code class="language-typescript">import { injectable, inject, container } from 'tsyringe';
interface AuthStrategy {
authenticate(username: string, password: string): Promise<boolean>;
}
@injectable()
class JWTAuthStrategy implements AuthStrategy {
async authenticate(username: string, password: string): Promise<boolean> {
console.log(Autenticando ${username} com JWT...);
return password === 'senha_correta';
}
}
@injectable()
class OAuth2Strategy implements AuthStrategy {
async authenticate(username: string, password: string): Promise<boolean> {
console.log(Autenticando ${username} com OAuth2...);
return true; // Simulado
}
}
@injectable()
class AuthService {
constructor(@inject('AuthStrategy') private strategy: AuthStrategy) {}
async login(username: string, password: string): Promise<boolean> {
return this.strategy.authenticate(username, password);
}
}
// Registrar a estratégia desejada
const strategyType = process.env.AUTH_STRATEGY || 'jwt';
if (strategyType === 'oauth2') {
container.register<AuthStrategy>('AuthStrategy', {
useClass: OAuth2Strategy,
});
} else {
container.register<AuthStrategy>('AuthStrategy', {
useClass: JWTAuthStrategy,
});
}
container.registerSingleton(AuthService);
// Usar
const authService = container.resolve(AuthService);
authService.login('usuario', 'senha_correta').then((success) => {
console.log(Login bem-sucedido: ${success});
});</code></pre>
<p>Esse padrão permite trocar a estratégia apenas alterando uma variável de ambiente, mantendo o <code>AuthService</code> totalmente desacoplado.</p>
<h3>Exemplo: Sistema de Notificações com InversifyJS</h3>
<p>Um caso mais complexo onde você envia notificações por diferentes canais. InversifyJS brilha quando você precisa de factories e escopos avançados.</p>
<pre><code class="language-typescript">import { Container, injectable, inject, interfaces } from 'inversify';
const TYPES = {
NotificationService: Symbol.for('NotificationService'),
EmailSender: Symbol.for('EmailSender'),
SMSSender: Symbol.for('SMSSender'),
NotificationFactory: Symbol.for('NotificationFactory'),
};
interface NotificationSender {
send(to: string, message: string): Promise<void>;
}
@injectable()
class EmailSender implements NotificationSender {
async send(to: string, message: string): Promise<void> {
console.log(Email enviado para ${to}: ${message});
}
}
@injectable()
class SMSSender implements NotificationSender {
async send(to: string, message: string): Promise<void> {
console.log(SMS enviado para ${to}: ${message});
}
}
@injectable()
class NotificationService {
constructor(
@inject(TYPES.EmailSender) private emailSender: NotificationSender,
@inject(TYPES.SMSSender) private smsSender: NotificationSender
) {}
async notifyByEmail(email: string, message: string): Promise<void> {
await this.emailSender.send(email, message);
}
async notifyBySMS(phone: string, message: string): Promise<void> {
await this.smsSender.send(phone, message);
}
}
// Factory para criar senders sob demanda
const notificationFactory = (context: interfaces.Context) => {
return {
sendEmail: (to: string, msg: string) =>
context.container.get<NotificationSender>(TYPES.EmailSender).send(to, msg),
sendSMS: (to: string, msg: string) =>
context.container.get<NotificationSender>(TYPES.SMSSender).send(to, msg),
};
};
// Configurar container
const container = new Container();
container.bind<NotificationSender>(TYPES.EmailSender).to(EmailSender).inSingletonScope();
container.bind<NotificationSender>(TYPES.SMSSender).to(SMSSender).inSingletonScope();
container.bind<NotificationService>(TYPES.NotificationService).to(NotificationService);
container
.bind(TYPES.NotificationFactory)
.toFactory<any>(notificationFactory);
// Usar
const notificationService = container.get<NotificationService>(
TYPES.NotificationService
);
notificationService.notifyByEmail('user@example.com', 'Bem-vindo!');
notificationService.notifyBySMS('+5511999999999', 'Código: 123456');</code></pre>
<p>Neste exemplo, cada sender é um singleton, economizando recursos, e você pode adicionar novos canais de notificação sem modificar <code>NotificationService</code>.</p>
<h2>Boas Práticas e Armadilhas Comuns</h2>
<h3>Evitar Over-Engineering</h3>
<p>Um erro frequente é criar abstrações para tudo, mesmo quando não é necessário. Se uma classe nunca será substituída, não precisa de uma interface. Inversão de dependência é uma ferramenta, não um dogma.</p>
<pre><code class="language-typescript"></code></pre>
<h3>Gerenciar Ciclos de Vida Corretamente</h3>
<p>Escopos (singleton, transient, request) afetam profundamente o comportamento da aplicação. Use singleton para serviços stateless; use transient quando cada requisição precisa de uma nova instância.</p>
<pre><code class="language-typescript">// tsyringe
container.registerSingleton(DatabasePool); // Uma instância para toda a app
container.register(UserSession); // Nova instância cada vez
// InversifyJS
container.bind<DatabasePool>(TYPES.DatabasePool)
.to(DatabasePool)
.inSingletonScope(); // Uma instância
container.bind<UserSession>(TYPES.UserSession)
.to(UserSession)
.inTransientScope(); // Nova instância cada vez</code></pre>
<h3>Testar com DI</h3>
<p>A verdadeira vantagem da inversão de dependência aparece nos testes. Você injeta mocks facilmente.</p>
<pre><code class="language-typescript">import { container } from 'tsyringe';
describe('UserRepository', () => {
it('deve buscar usuários do banco de dados', async () => {
const mockDb: DatabaseConnection = {
connect: jest.fn().mockResolvedValue(undefined),
query: jest
.fn()
.mockResolvedValue([{ id: 1, name: 'Teste' }]),
};
container.register<DatabaseConnection>('DatabaseConnection', {
useValue: mockDb,
});
const userRepo = container.resolve(UserRepository);
const users = await userRepo.getUsers();
expect(users).toHaveLength(1);
expect(users[0].name).toBe('Teste');
});
});</code></pre>
<h2>Conclusão</h2>
<p>Aprendemos que <strong>inversão de dependência</strong> é sobre depender de abstrações, não de implementações concretas. O <strong>tsyringe</strong> oferece simplicidade e é perfeito para a maioria dos projetos, enquanto o <strong>InversifyJS</strong> fornece controle avançado para aplicações complexas. A chave está em usar a ferramenta certa para o problema certo — nem mais, nem menos — e sempre pensar em testes desde o início do design.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://github.com/Microsoft/tsyringe" target="_blank" rel="noopener noreferrer">tsyringe - GitHub Oficial</a></li>
<li><a href="https://inversify.io/" target="_blank" rel="noopener noreferrer">InversifyJS - Documentação Oficial</a></li>
<li><a href="https://www.oreilly.com/library/view/clean-code-a/9780136083238/" target="_blank" rel="noopener noreferrer">SOLID Principles - Clean Code</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/decorators.html" target="_blank" rel="noopener noreferrer">Dependency Injection in TypeScript</a></li>
<li><a href="https://www.refactoring.guru/design-patterns" target="_blank" rel="noopener noreferrer">Padrões de Projeto em JavaScript/TypeScript</a></li>
</ul>
<p><!-- FIM --></p>