DDD: Anti-Corruption Layer - Protegendo seu domínio (Parte 10)

DDD9 min de leitura

Este é o décimo artigo da série sobre Domain-Driven Design. Nos artigos anteriores, exploramos Context Mapping e os diferentes padrões de relacionamento entre contextos. Agora vamos nos aprofundar em um padrão específico e crucial: Anti-Corruption Layer (ACL).

A Anti-Corruption Layer é uma das ferramentas mais importantes para manter a integridade do seu modelo de domínio quando você precisa integrar com sistemas externos ou legados.

O que é Anti-Corruption Layer?

Anti-Corruption Layer é um padrão de integração que cria uma camada isolada para traduzir entre dois modelos de domínio diferentes, protegendo o modelo interno das influências externas.

Eric Evans define:

"Uma camada anti-corrupção é um meio de proporcionar aos clientes funcionalidade em termos de seu próprio modelo de domínio."

A ACL atua como um tradutor e protetor, garantindo que:

  • Seu modelo de domínio permaneça limpo e consistente
  • Mudanças no sistema externo não quebrem seu sistema
  • A integração seja testável e mantível

Por que usar Anti-Corruption Layer?

Problemas sem ACL

Sem uma ACL, integração direta pode causar:

<?php

// ❌ PROBLEMA: Modelo corrompido por sistema externo
class PedidoService
{
    public function __construct(
        private ErpService $erpService,
        private PedidoRepository $pedidoRepository
    ) {}
    
    public function criarPedido(CriarPedidoRequest $dados): void
    {
        // Nosso modelo limpo...
        $pedido = new Pedido($dados->clienteId, $dados->itens);
        
        // ...mas somos forçados a usar estruturas do sistema externo
        $clienteERP = $this->erpService->getCustomer($dados->clienteId);
        
        // Lógica de negócio misturada com tradução
        $pedidoERP = [
            'cust_id' => $clienteERP['id'],
            'cust_name' => $clienteERP['customer_name'],
            'order_date' => date('Y-m-d'), // formato YYYY-MM-DD
            'items' => array_map(function($item) {
                return [
                    'product_code' => $item->produtoId,
                    'qty' => $item->quantidade,
                    'unit_price' => $item->preco * 100 // centavos
                ];
            }, $dados->itens)
        ];
        
        $this->erpService->createOrder($pedidoERP);
        $this->pedidoRepository->salvar($pedido);
    }
}

Soluções com ACL

Com ACL, mantemos nosso modelo limpo:

<?php

// ✅ SOLUÇÃO: Modelo protegido por ACL
class PedidoService
{
    public function __construct(
        private PedidoRepository $pedidoRepository,
        private ClienteService $clienteService,
        private ErpAdapter $erpAdapter // ⭐ ACL
    ) {}
    
    public function criarPedido(CriarPedidoRequest $dados): void
    {
        // Nosso modelo permanece limpo
        $cliente = $this->clienteService->buscarPorId($dados->clienteId);
        $pedido = new Pedido($cliente, $dados->itens);
        
        $this->pedidoRepository->salvar($pedido);
        
        // ACL cuida da tradução e integração
        $this->erpAdapter->registrarPedido($pedido);
    }
}

// Anti-Corruption Layer
class ErpAdapter
{
    public function __construct(private ERPService $erpService) {}
    
    public function registrarPedido(Pedido $pedido): void
    {
        // Traduz nosso modelo para o formato do ERP
        $pedidoERP = $this->traduzirPedido($pedido);
        $this->erpService->createOrder($pedidoERP);
    }
    
    private function traduzirPedido(Pedido $pedido): array
    {
        return [
            'cust_id' => $pedido->cliente->id,
            'cust_name' => $pedido->cliente->nome,
            'order_date' => $this->formatarData($pedido->dataCriacao),
            'items' => array_map([$this, 'traduzirItem'], $pedido->itens)
        ];
    }
    
    private function traduzirItem(ItemPedido $item): array
    {
        return [
            'product_code' => $item->produto->codigo,
            'qty' => $item->quantidade,
            'unit_price' => round($item->precoUnitario->valor * 100)
        ];
    }
    
    private function formatarData(DateTime $data): string
    {
        return $data->format('Y-m-d');
    }
}

Padrões de Implementação da ACL

1. Adapter Pattern

Adaptador simples para sistemas com interfaces bem definidas:

<?php

// Interface do nosso domínio
interface ClienteRepository
{
    public function buscarPorId(string $id): ?Cliente;
    public function salvar(Cliente $cliente): void;
}

// Adapter que implementa nossa interface
class ClienteERPAdapter implements ClienteRepository
{
    public function __construct(private ERPCustomerService $erpService) {}
    
    public function buscarPorId(string $id): ?Cliente
    {
        $customerERP = $this->erpService->getCustomer($id);
        return $this->traduzirCliente($customerERP);
    }
    
    public function salvar(Cliente $cliente): void
    {
        $customerERP = $this->traduzirParaERP($cliente);
        $this->erpService->updateCustomer($customerERP);
    }
    
    private function traduzirCliente(array $customerERP): Cliente
    {
        return new Cliente(
            $customerERP['id'],
            $customerERP['customer_name'],
            new Email($customerERP['email_address']),
            $this->traduzirTipo($customerERP['customer_type'])
        );
    }
    
    private function traduzirParaERP(Cliente $cliente): array
    {
        return [
            'id' => $cliente->id,
            'customer_name' => $cliente->nome,
            'email_address' => $cliente->email->valor,
            'customer_type' => $cliente->tipo === TipoCliente::FISICA ? 'P' : 'J'
        ];
    }
    
    private function traduzirTipo(string $tipo): TipoCliente
    {
        return $tipo === 'P' ? TipoCliente::FISICA : TipoCliente::JURIDICA;
    }
}

2. Facade Pattern

Para sistemas com múltiplas interfaces relacionadas:

// Facade que unifica acesso a múltiplos sistemas
class SistemaFinanceiroFacade {
  constructor(
    private erpService: ERPService,
    private bancarioService: SistemaBancarioService,
    private contabilService: SistemaContabilService
  ) {}
  
  async processarPagamento(pagamento: Pagamento): Promise<ResultadoPagamento> {
    // Coordena operações em múltiplos sistemas
    
    // 1. Registra no ERP
    await this.erpService.createPayment({
      payment_id: pagamento.id,
      amount_cents: Math.round(pagamento.valor.valor * 100),
      customer_id: pagamento.clienteId
    });
    
    // 2. Efetua transferência bancária
    const transferencia = await this.bancarioService.transfer({
      from_account: pagamento.contaOrigem,
      to_account: pagamento.contaDestino,
      amount: pagamento.valor.valor,
      reference: pagamento.id
    });
    
    // 3. Registra na contabilidade
    await this.contabilService.recordTransaction({
      transaction_id: transferencia.id,
      account_debit: pagamento.contaOrigem,
      account_credit: pagamento.contaDestino,
      amount: pagamento.valor.valor,
      description: `Pagamento ${pagamento.id}`
    });
    
    return new ResultadoPagamento(
      pagamento.id,
      transferencia.status === 'completed' ? StatusPagamento.APROVADO : StatusPagamento.REJEITADO,
      transferencia.id
    );
  }
}

3. Gateway Pattern

Para acesso a recursos externos com tradução de dados:

// Gateway para serviço de CEP externo
interface EnderecoGateway {
  buscarPorCep(cep: string): Promise<Endereco>;
}

class ViaCepGateway implements EnderecoGateway {
  constructor(private httpClient: HttpClient) {}
  
  async buscarPorCep(cep: string): Promise<Endereco> {
    const cepLimpo = cep.replace(/\D/g, '');
    
    if (cepLimpo.length !== 8) {
      throw new Error('CEP inválido');
    }
    
    try {
      const response = await this.httpClient.get(`https://viacep.com.br/ws/${cepLimpo}/json/`);
      
      if (response.erro) {
        throw new Error('CEP não encontrado');
      }
      
      return this.traduzirEndereco(response);
    } catch (error) {
      throw new Error(`Erro ao buscar CEP: ${error.message}`);
    }
  }
  
  private traduzirEndereco(viaCepResponse: any): Endereco {
    return new Endereco(
      viaCepResponse.cep.replace('-', ''),
      viaCepResponse.logradouro,
      '', // número não vem da API
      viaCepResponse.complemento,
      viaCepResponse.bairro,
      viaCepResponse.localidade,
      viaCepResponse.uf
    );
  }
}

4. Repository Pattern com ACL

Para persistência com tradução de modelos:

// Repository com ACL para banco de dados legado
class ProdutoRepositoryACL implements ProdutoRepository {
  constructor(private legacyDB: LegacyDatabase) {}
  
  async buscarPorId(id: string): Promise<Produto | null> {
    const query = `
      SELECT p.prod_id, p.prod_name, p.prod_desc, p.unit_price, p.stock_qty,
             c.cat_id, c.cat_name
      FROM products p
      INNER JOIN categories c ON p.cat_id = c.cat_id
      WHERE p.prod_id = ?
    `;
    
    const resultado = await this.legacyDB.query(query, [id]);
    
    if (!resultado.length) {
      return null;
    }
    
    return this.traduzirProduto(resultado[0]);
  }
  
  async salvar(produto: Produto): Promise<void> {
    const produtoLegacy = this.traduzirParaLegacy(produto);
    
    const query = `
      UPDATE products 
      SET prod_name = ?, prod_desc = ?, unit_price = ?, stock_qty = ?
      WHERE prod_id = ?
    `;
    
    await this.legacyDB.execute(query, [
      produtoLegacy.prod_name,
      produtoLegacy.prod_desc,
      produtoLegacy.unit_price,
      produtoLegacy.stock_qty,
      produtoLegacy.prod_id
    ]);
  }
  
  private traduzirProduto(row: any): Produto {
    const categoria = new Categoria(row.cat_id, row.cat_name);
    
    return new Produto(
      row.prod_id,
      row.prod_name,
      row.prod_desc,
      new Dinheiro(row.unit_price / 100), // legacy armazena em centavos
      row.stock_qty,
      categoria
    );
  }
  
  private traduzirParaLegacy(produto: Produto): any {
    return {
      prod_id: produto.id,
      prod_name: produto.nome,
      prod_desc: produto.descricao,
      unit_price: Math.round(produto.preco.valor * 100),
      stock_qty: produto.quantidadeEstoque,
      cat_id: produto.categoria.id
    };
  }
}

Tratamento de Erros na ACL

A ACL deve tratar erros do sistema externo e traduzi-los para o contexto do domínio:

class PagamentoGatewayACL {
  constructor(private pagamentoExterno: PagamentoExternoAPI) {}
  
  async processarPagamento(pagamento: Pagamento): Promise<ResultadoPagamento> {
    try {
      const resultado = await this.pagamentoExterno.pay({
        amount: pagamento.valor.valor,
        currency: 'BRL',
        card_token: pagamento.cartao.token
      });
      
      return this.traduzirResultado(resultado);
      
    } catch (error) {
      // Traduz erros externos para exceções do domínio
      throw this.traduzirErro(error);
    }
  }
  
  private traduzirResultado(resultado: any): ResultadoPagamento {
    const status = this.traduzirStatus(resultado.status);
    return new ResultadoPagamento(resultado.transaction_id, status);
  }
  
  private traduzirStatus(statusExterno: string): StatusPagamento {
    switch (statusExterno) {
      case 'approved': return StatusPagamento.APROVADO;
      case 'declined': return StatusPagamento.REJEITADO;
      case 'pending': return StatusPagamento.PENDENTE;
      default: return StatusPagamento.ERRO;
    }
  }
  
  private traduzirErro(error: any): Error {
    if (error.code === 'INVALID_CARD') {
      return new CartaoInvalidoError('Cartão de crédito inválido');
    }
    
    if (error.code === 'INSUFFICIENT_FUNDS') {
      return new SaldoInsuficienteError('Saldo insuficiente');
    }
    
    if (error.code === 'NETWORK_ERROR') {
      return new ServicoIndisponivelError('Serviço de pagamento indisponível');
    }
    
    return new PagamentoError(`Erro no pagamento: ${error.message}`);
  }
}

ACL com Cache

Para otimizar performance em integrações custosas:

class ClienteACLComCache implements ClienteRepository {
  private cache = new Map<string, Cliente>();
  private readonly TTL = 5 * 60 * 1000; // 5 minutos
  
  constructor(
    private erpService: ERPService,
    private cacheTimestamps = new Map<string, number>()
  ) {}
  
  async buscarPorId(id: string): Promise<Cliente | null> {
    // Verifica cache primeiro
    if (this.isValidCache(id)) {
      return this.cache.get(id) || null;
    }
    
    // Busca no sistema externo
    const clienteERP = await this.erpService.getCustomer(id);
    
    if (!clienteERP) {
      return null;
    }
    
    // Traduz e armazena no cache
    const cliente = this.traduzirCliente(clienteERP);
    this.cache.set(id, cliente);
    this.cacheTimestamps.set(id, Date.now());
    
    return cliente;
  }
  
  async salvar(cliente: Cliente): Promise<void> {
    const clienteERP = this.traduzirParaERP(cliente);
    await this.erpService.updateCustomer(clienteERP);
    
    // Invalida cache
    this.cache.delete(cliente.id);
    this.cacheTimestamps.delete(cliente.id);
  }
  
  private isValidCache(id: string): boolean {
    if (!this.cache.has(id)) return false;
    
    const timestamp = this.cacheTimestamps.get(id) || 0;
    return Date.now() - timestamp < this.TTL;
  }
  
  private traduzirCliente(clienteERP: ERPCustomer): Cliente {
    return new Cliente(
      clienteERP.id,
      clienteERP.customer_name,
      new Email(clienteERP.email_address)
    );
  }
  
  private traduzirParaERP(cliente: Cliente): ERPCustomer {
    return {
      id: cliente.id,
      customer_name: cliente.nome,
      email_address: cliente.email.valor
    };
  }
}

Testes da ACL

A ACL deve ser extensivamente testada:

describe('ClienteERPAdapter', () => {
  let adapter: ClienteERPAdapter;
  let mockERPService: jest.Mocked<ERPCustomerService>;
  
  beforeEach(() => {
    mockERPService = {
      getCustomer: jest.fn(),
      updateCustomer: jest.fn()
    };
    adapter = new ClienteERPAdapter(mockERPService);
  });
  
  describe('buscarPorId', () => {
    it('deve traduzir cliente do ERP corretamente', async () => {
      // Arrange
      const clienteERP = {
        id: '123',
        customer_name: 'João Silva',
        email_address: 'joao@email.com',
        customer_type: 'P'
      };
      mockERPService.getCustomer.mockResolvedValue(clienteERP);
      
      // Act
      const cliente = await adapter.buscarPorId('123');
      
      // Assert
      expect(cliente).toBeDefined();
      expect(cliente!.id).toBe('123');
      expect(cliente!.nome).toBe('João Silva');
      expect(cliente!.email.valor).toBe('joao@email.com');
      expect(cliente!.tipo).toBe(TipoCliente.FISICA);
    });
    
    it('deve tratar erro do ERP graciosamente', async () => {
      // Arrange
      mockERPService.getCustomer.mockRejectedValue(new Error('ERP Error'));
      
      // Act & Assert
      await expect(adapter.buscarPorId('123')).rejects.toThrow('ERP Error');
    });
  });
  
  describe('salvar', () => {
    it('deve traduzir cliente para formato do ERP', async () => {
      // Arrange
      const cliente = new Cliente(
        '123',
        'Maria Santos',
        new Email('maria@email.com'),
        TipoCliente.JURIDICA
      );
      
      // Act
      await adapter.salvar(cliente);
      
      // Assert
      expect(mockERPService.updateCustomer).toHaveBeenCalledWith({
        id: '123',
        customer_name: 'Maria Santos',
        email_address: 'maria@email.com',
        customer_type: 'J'
      });
    });
  });
});

Conclusão

A Anti-Corruption Layer é uma ferramenta essencial para manter a integridade do modelo de domínio em sistemas que precisam integrar com sistemas externos ou legados.

Pontos-chave:

  1. ACL protege seu modelo de influências externas
  2. Tradução bidirecional entre modelos diferentes
  3. Tratamento de erros específico do domínio
  4. Testabilidade através de isolamento
  5. Performance pode ser otimizada com cache
  6. Múltiplos padrões (Adapter, Facade, Gateway) podem ser usados

Quando usar ACL:

  • ✅ Integração com sistemas legados
  • ✅ APIs externas com modelos diferentes
  • ✅ Múltiplos sistemas com estruturas divergentes
  • ✅ Proteção contra mudanças externas

Quando não usar:

  • ❌ Sistemas com modelos compatíveis
  • ❌ Integrações simples e estáveis
  • ❌ Overhead desnecessário

Uma ACL bem implementada resulta em um modelo de domínio mais limpo, estável e testável, protegendo seu investimento em arquitetura limpa.

No próximo artigo da série, exploraremos Specification Pattern.

Referências