DDD: Anti-Corruption Layer - Protegendo seu domínio (Parte 10)
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:
- ACL protege seu modelo de influências externas
- Tradução bidirecional entre modelos diferentes
- Tratamento de erros específico do domínio
- Testabilidade através de isolamento
- Performance pode ser otimizada com cache
- 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.