Sobes.tech
Senior

Como resolver o problema do uso de dependências de componentes periféricos no núcleo?

sobes.tech IA

Resposta da IA

Resolver o problema do uso de dependências de componentes periféricos (adaptadores, interfaces) no núcleo (lógica de domínio) em arquiteturas como a Arquitetura Limpa ou DDD, pode ser feito usando o princípio de Inversão de Dependências (Dependency Inversion Principle - DIP).

A essência do DIP:

  1. Os módulos de nível superior não devem depender dos módulos de nível inferior. Ambos devem depender de abstrações.
  2. As abstrações não devem depender dos detalhes. Os detalhes devem depender das abstrações.

Aplicação prática:

  • O núcleo declara interfaces: O núcleo define abstrações (interfaces ou classes abstratas) que descrevem as funções necessárias dos componentes periféricos.
  • Os componentes periféricos implementam interfaces: Os módulos externos (adaptadores para bases de dados, clientes HTTP, sistemas de arquivos, etc.) implementam essas interfaces.
  • Injeção de dependências (Dependency Injection - DI): Os componentes periféricos são injetados no núcleo de fora, geralmente em um nível superior da aplicação (por exemplo, na raiz de composição ou usando um contêiner DI). O núcleo trabalha apenas com abstrações.

Exemplo de uso do DIP em Node.js:

Suponha que temos um núcleo que precisa de acesso a dados.

// src/domain/interfaces/UserRepository.ts
// O núcleo declara a interface
export interface UserRepository {
  getUserById(id: string): Promise<User | null>;
}
// src/domain/entities/User.ts
// Entidade do núcleo
export class User {
  constructor(public id: string, public name: string) {}
}
// src/domain/usecases/GetUserUseCase.ts
// Caso de uso do núcleo depende da abstração UserRepository
import { UserRepository } from '../interfaces/UserRepository';
import { User } from '../entities/User';

export class GetUserUseCase {
  constructor(private userRepository: UserRepository) {}

  async execute(id: string): Promise<User | null> {
    return this.userRepository.getUserById(id);
  }
}
// src/infrastructure/database/MongoDbUserRepository.ts
// Componente periférico que implementa a interface do núcleo
import { UserRepository } from '../../domain/interfaces/UserRepository';
import { User } from '../../domain/entities/User';

export class MongoDbUserRepository implements UserRepository {
  // Aqui estará a lógica de interação com MongoDB

  async getUserById(id: string): Promise<User | null> {
    console.log(`Fetching user with id ${id} from MongoDB...`);
    // Simulação de obtenção de dados
    if (id === '123') {
      return new User('123', 'John Doe');
    }
    return null;
  }
}
// src/app/CompositionRoot.ts
// A raiz de composição monta as dependências
import { GetUserUseCase } from '../domain/usecases/GetUserUseCase';
import { MongoDbUserRepository } from '../infrastructure/database/MongoDbUserRepository';

// Injeção de dependência: O núcleo recebe a implementação MongoDbUserRepository via interface
const userRepository = new MongoDbUserRepository();
const getUserUseCase = new GetUserUseCase(userRepository);

// Agora podemos usar o caso de uso
async function run() {
  const user = await getUserUseCase.execute('123');
  if (user) {
    console.log(`Found user: ${user.name}`);
  } else {
    console.log('User not found');
  }
}

run();

Vantagens dessa abordagem:

  • Independência do núcleo: O núcleo não conhece as implementações específicas dos componentes periféricos.
  • Testabilidade: O núcleo é fácil de testar, substituindo implementações mock das interfaces.
  • Flexibilidade: Pode-se facilmente substituir uma implementação (por exemplo, banco de dados) por outra, sem alterar o código do núcleo.
  • Manutenção: Facilita a manutenção e alteração do código.