🏛️ Capítulo 18: Clean Architecture e Arquitetura Hexagonal
Módulo 10: Paradigmas de Programação & Padrões de Projeto • Bloco 5: Arquitetura e Enterprise Patterns
🎯 O Negócio no Centro, a Tecnologia na Borda: Frameworks, bancos de dados e provedores de nuvem mudam a cada poucos anos; as regras que fazem uma empresa ganhar dinheiro mudam muito mais devagar. As arquiteturas limpas — Hexagonal (Cockburn, 2005), Onion (Palermo, 2008) e Clean Architecture (Martin, 2012) — organizam o código para que o núcleo de negócio não dependa de nenhum detalhe técnico. Neste capítulo, vemos como a Regra da Dependência e o padrão Ports & Adapters transformam banco, web e mensageria em peças substituíveis.
🗺️ Mapa Conceitual do Capítulo
flowchart TD
CLEAN["Arquiteturas Limpas e Desacopladas"]
CLEAN --> RULE["1. Regra da Dependência (Uncle Bob)<br/>• Dependências de código apontam só para dentro<br/>• O núcleo não conhece frameworks<br/>• Inversão via interfaces (DIP)"]
CLEAN --> ENT["2. Entidades & Casos de Uso (Core Puro)<br/>• Entidades: regras corporativas invariantes<br/>• Casos de uso: fluxos da aplicação<br/>• Testáveis sem banco, web ou rede"]
CLEAN --> HEX["3. Hexagonal (Ports & Adapters)<br/>• Portas de entrada (driving) e saída (driven)<br/>• Adaptadores: HTTP, CLI, SQL, filas<br/>• Troca de tecnologia sem tocar no core"]
style CLEAN fill:#ede7f6,stroke:#512da8,stroke-width:2px
style RULE fill:#e8f5e9,stroke:#4caf50,stroke-width:1.5px
style ENT fill:#fff3e0,stroke:#ff9800,stroke-width:1.5px
style HEX fill:#e1f5fe,stroke:#03a9f4,stroke-width:1.5px
🧱 1. O Problema: Arquitetura em Camadas Acoplada ao Banco
Na arquitetura em camadas tradicional (Controller → Service → Repository → Banco), a dependência de código desce até a infraestrutura. O resultado típico:
- As entidades de negócio são classes do ORM (
class Pedido(db.Model)), carregando anotações e sessões do framework; - Testar uma regra simples de desconto exige subir um banco de dados;
- Migrar de MySQL para PostgreSQL, ou de REST para mensageria, obriga a alterar a camada de serviço.
“The architecture should scream the intent of the system, not the frameworks it uses.” — Robert C. Martin, Clean Architecture (2017)
Olhando a estrutura de pastas de um bom sistema, deveríamos ler fechar_pedido, calcular_frete, emitir_nota — e não controllers, models, migrations.
🎯 2. A Regra da Dependência (The Dependency Rule)
“Source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle.” — Robert C. Martin
flowchart TD
subgraph L4["Frameworks & Drivers (Web, DB, Devices)"]
subgraph L3["Interface Adapters (Controllers, Gateways, Presenters)"]
subgraph L2["Application Business Rules (Use Cases)"]
subgraph L1["Enterprise Business Rules (Entities)"]
E["Entidades Puras"]
end
UC["Casos de Uso"]
end
ADP["Controladores & Repositórios"]
end
FW["HTTP / SQL / Redis / CLI"]
end
FW --> ADP --> UC --> E
style L1 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style L2 fill:#fff9c4,stroke:#fbc02d,stroke-width:2px
style L3 fill:#ffe0b2,stroke:#f57c00,stroke-width:2px
style L4 fill:#bbdefb,stroke:#1976d2,stroke-width:2px
| Círculo | Contém | Muda quando… | Pode importar |
|---|---|---|---|
| Entidades | Regras corporativas (Conta.debitar, Pedido.total) |
A política da empresa muda | Apenas a linguagem |
| Casos de Uso | Fluxos da aplicação (TransferirDinheiro) |
O processo da aplicação muda | Entidades + portas |
| Adaptadores | Controllers, presenters, repositórios concretos | O formato de entrada/saída muda | Casos de uso |
| Frameworks & Drivers | FastAPI, Spring, PostgreSQL, Kafka | A tecnologia muda | Tudo |
2.1 Como o Fluxo Sai do Centro sem Violar a Regra?
O caso de uso precisa salvar dados, mas não pode importar o repositório SQL. A solução é o DIP (Capítulo 6): o núcleo declara uma interface e o adaptador externo a implementa. O fluxo de controle vai para fora, mas a dependência de código aponta para dentro:
# núcleo: declara o que precisa
class ContaRepositorioPort(ABC):
@abstractmethod
def obter(self, numero: str) -> Conta: ...
# borda: implementa com a tecnologia escolhida
class RepositorioSQLite(ContaRepositorioPort):
def obter(self, numero):
linha = self._db.execute("SELECT ... WHERE numero = ?", (numero,)).fetchone()
return Conta(*linha)
⬡ 3. Arquitetura Hexagonal (Ports & Adapters)
Criada por Alistair Cockburn (2005), a Arquitetura Hexagonal descreve a mesma ideia com outra metáfora: a aplicação é um hexágono cujos lados são portas, e cada tecnologia se conecta por um adaptador.
flowchart LR
subgraph Entrada["Adaptadores de Entrada (driving)"]
HTTP["API HTTP"]
CLI["CLI"]
TST["Teste automatizado"]
end
subgraph Core["⬡ Núcleo da Aplicação"]
IN(["Porta de entrada<br/>TransferirDinheiro"])
DOM["Entidades<br/>Conta"]
OUT1(["Porta de saída<br/>ContaRepositorioPort"])
OUT2(["Porta de saída<br/>NotificacaoPort"])
IN --> DOM
IN --> OUT1
IN --> OUT2
end
subgraph Saida["Adaptadores de Saída (driven)"]
PG["PostgreSQL / SQLite"]
MEM["Repositório em memória"]
PUSH["Push / E-mail"]
end
HTTP --> IN
CLI --> IN
TST --> IN
PG -.->|implementa| OUT1
MEM -.->|implementa| OUT1
PUSH -.->|implementa| OUT2
- Portas de entrada (driving/primary): a API que o núcleo oferece ao mundo — normalmente os próprios casos de uso.
- Portas de saída (driven/secondary): o que o núcleo precisa do mundo — persistência, notificações, pagamentos, relógio.
- Adaptadores: traduzem entre o protocolo externo (JSON, SQL, AMQP) e as portas. Podem ser trocados em tempo de configuração, inclusive por versões em memória nos testes.
3.1 Hexagonal, Onion e Clean: Mesma Família
| Aspecto | Hexagonal (2005) | Onion (2008) | Clean (2012) |
|---|---|---|---|
| Metáfora | Hexágono com portas | Camadas de cebola | Círculos concêntricos |
| Foco | Simetria entre entrada e saída | Domínio no centro, serviços ao redor | Regra da Dependência explícita |
| Contribuição principal | Vocabulário ports & adapters | Domain Services | Casos de uso e Presenters |
⚖️ 4. Custos, Armadilhas e Pragmatismo
- Cerimônia: para um CRUD simples, portas + adaptadores + DTOs podem triplicar o número de arquivos sem benefício. Reserve a arquitetura completa para domínios com regras ricas.
- Mapeamento entre camadas: entidades de domínio e modelos do ORM são objetos diferentes, exigindo conversões (mappers).
- “Arquitetura de pastas”: criar diretórios
domain/einfra/não basta; se o domínio importa o ORM, a regra foi violada. Verifique automaticamente com ferramentas como import-linter (Python), ArchUnit (Java) e dependency-cruiser (JavaScript). - Transações: o limite transacional normalmente fica no caso de uso, exposto por uma porta Unit of Work, para não vazar sessões de banco para o núcleo.
💡 Regra prática: se você consegue rodar todos os testes dos casos de uso sem Docker, sem rede e sem banco, o núcleo está realmente isolado.