Clean Architecture: as camadas e a regra de dependência
1. Introdução à Clean Architecture
A Clean Architecture foi proposta por Robert C. Martin (Uncle Bob) em 2012, como resposta à crescente complexidade dos sistemas de software modernos. A motivação central é clara: isolar as regras de negócio dos detalhes técnicos — frameworks, bancos de dados, interfaces de usuário e APIs externas. O objetivo é criar sistemas que sejam testáveis, independentes de tecnologia e que evoluam sem dor.
O diagrama clássico da Clean Architecture é composto por círculos concêntricos. No centro, estão as regras de negócio mais puras. Conforme avançamos para as camadas externas, encontramos mecanismos cada vez mais específicos e voláteis. A grande inovação não está nos círculos em si, mas na regra de dependência que os governa.
2. As Quatro Camadas Principais
A Clean Architecture define quatro camadas principais, de dentro para fora:
Entities (Entidades): No centro absoluto. Representam as regras de negócio corporativas — aquelas que existiriam mesmo se o sistema fosse executado em papel. Exemplos: Pedido, Cliente, Produto. Elas não conhecem bancos de dados, APIs ou frameworks.
Use Cases (Casos de Uso): Contêm as regras de negócio específicas da aplicação. Orquestram o fluxo de dados entre entidades e coordenam operações. Um caso de uso "CriarPedido" sabe que precisa validar estoque e calcular impostos, mas não sabe como esses dados são persistidos.
Interface Adapters (Adaptadores de Interface): Traduzem dados entre o formato mais conveniente para os casos de uso e o formato exigido por bancos de dados, UI ou APIs externas. Exemplos: Controllers, Presenters, Gateways de repositório.
Frameworks & Drivers: A camada mais externa. Contém frameworks web, drivers de banco de dados, bibliotecas de terceiros. É onde a "sujeira" fica confinada.
3. A Regra de Dependência (Dependency Rule)
A regra de dependência é o coração da Clean Architecture. Ela afirma: as dependências de código-fonte devem apontar apenas para dentro, em direção ao centro do círculo. Ou seja, nada em um círculo interno pode saber algo sobre um círculo externo.
Na prática, isso significa:
- Entities não sabem da existência de Use Cases.
- Use Cases não sabem da existência de Controllers ou bancos de dados.
- Interface Adapters sabem sobre Use Cases, mas não sobre Frameworks.
Se uma camada externa precisa se comunicar com uma interna, isso deve acontecer por meio de interfaces definidas na camada interna e implementadas na externa. É a inversão de dependência em ação.
4. Fluxo de Controle vs. Fluxo de Dependência
Há uma distinção crucial entre fluxo de controle (como o programa executa) e fluxo de dependência (como o código se relaciona).
- Fluxo de controle: começa na camada externa (ex: um Controller recebe uma requisição HTTP) e flui para dentro (chama um Use Case, que manipula Entities).
- Fluxo de dependência: as camadas internas definem interfaces; as externas as implementam. O Use Case depende de uma abstração (
PedidoRepository), não de uma implementação concreta (PostgresPedidoRepository).
Exemplo prático:
// Camada externa: Controller (Frameworks & Drivers)
class PedidoController:
function criar(request):
comando = CriarPedidoComando(request.dados)
casoDeUso = new CriarPedidoUseCase(repositorio)
resultado = casoDeUso.executar(comando)
return resposta(resultado)
O Controller chama o Use Case (fluxo de controle para dentro). Mas o Use Case depende de uma interface PedidoRepository definida na camada de Use Cases, e o Controller injeta uma implementação concreta. A dependência de código-fonte aponta para dentro.
5. Implementação da Regra com Interfaces e Injeção de Dependência
A implementação prática da regra de dependência exige dois mecanismos:
- Interfaces (Ports): definidas nas camadas internas. Exemplo: um Use Case declara
interface PedidoRepositorycom métodos comosalvar(Pedido). - Implementações concretas (Adapters): nas camadas externas. Exemplo:
PostgresPedidoRepositoryimplementaPedidoRepositoryusando SQL.
A injeção de dependência (DI) é o mecanismo que conecta tudo. O ponto de entrada do sistema (ex: main.go ou um contêiner DI) cria as implementações concretas e as injeta nos Use Cases.
// Camada interna: Use Case
interface PedidoRepository:
function salvar(pedido: Pedido): void
class CriarPedidoUseCase:
repositorio: PedidoRepository
function __construct(repositorio: PedidoRepository):
this.repositorio = repositorio
function executar(comando: CriarPedidoComando): Resultado:
pedido = new Pedido(comando.dados)
this.repositorio.salvar(pedido)
return Resultado.sucesso(pedido.id)
// Camada externa: Adapter
class PostgresPedidoRepository implements PedidoRepository:
function salvar(pedido: Pedido):
sql = "INSERT INTO pedidos ..."
// executa SQL
// Ponto de entrada: composição
function main():
repositorio = new PostgresPedidoRepository()
casoDeUso = new CriarPedidoUseCase(repositorio)
controller = new PedidoController(casoDeUso)
// inicia servidor web
6. Exemplo Prático: Sistema de Pedidos
Vamos estruturar um sistema de pedidos seguindo a Clean Architecture:
src/
entities/
Pedido.java
Item.java
Cliente.java
usecases/
CriarPedidoUseCase.java
interfaces/
PedidoRepository.java
interfaceAdapters/
controllers/
PedidoController.java
presenters/
PedidoPresenter.java
repositories/
PostgresPedidoRepository.java
frameworks/
database/
PostgresConnection.java
web/
SparkServer.java
Entity:
class Pedido:
id: string
cliente: Cliente
itens: List<Item>
status: string
dataCriacao: Date
function calcularTotal(): double:
return itens.soma(item => item.preco * item.quantidade)
Use Case:
class CriarPedidoUseCase:
repositorio: PedidoRepository
servicoEstoque: ServicoEstoque
function executar(comando: CriarPedidoComando): Resultado:
pedido = Pedido.criar(comando.clienteId, comando.itens)
if not servicoEstoque.validarDisponibilidade(pedido.itens):
return Resultado.falha("Estoque insuficiente")
repositorio.salvar(pedido)
return Resultado.sucesso(pedido.id)
Adapter de repositório:
class PostgresPedidoRepository implements PedidoRepository:
database: Database
function salvar(pedido: Pedido):
transacao = database.iniciarTransacao()
transacao.executar("INSERT INTO pedidos ...", pedido.id, ...)
for item in pedido.itens:
transacao.executar("INSERT INTO itens ...", item.id, ...)
transacao.commit()
Injeção no ponto de entrada:
function main():
database = new PostgresConnection("url", "user", "pass")
repositorio = new PostgresPedidoRepository(database)
servicoEstoque = new ServicoEstoqueApi("https://estoque.api")
casoDeUso = new CriarPedidoUseCase(repositorio, servicoEstoque)
controller = new PedidoController(casoDeUso)
Spark.port(8080)
Spark.post("/pedidos", controller::criar)
7. Comparação com Arquiteturas Vizinhas
A Arquitetura em Camadas tradicional (Layered Architecture) permite que uma camada dependa da camada imediatamente abaixo. Isso cria dependências ascendentes: a camada de negócios depende da camada de dados. O resultado é que mudanças no banco de dados frequentemente forçam mudanças nas regras de negócio.
A Onion Architecture (Jeffrey Palermo) e a Arquitetura Hexagonal (Alistair Cockburn) precederam a Clean Architecture e compartilham o mesmo princípio fundamental: dependências apontam para dentro. A Onion usa círculos concêntricos como a Clean Architecture. A Hexagonal define "portos" (interfaces) e "adaptadores" (implementações) nas bordas do sistema.
A Clean Architecture unifica esses conceitos, oferecendo uma nomenclatura clara (Entities, Use Cases, Interface Adapters, Frameworks) e enfatizando a regra de dependência como pilar central. Ela não inventa nada radicalmente novo, mas organiza e simplifica ideias que já existiam.
8. Considerações Finais e Boas Práticas
Implementar Clean Architecture exige disciplina. Alguns cuidados essenciais:
- Evite vazamento de detalhes: nunca importe bibliotecas de frameworks nas camadas internas. Se um Use Case precisa de data atual, use
LocalDateda linguagem, nãoDateTimedo framework de banco. - Avalie o custo-benefício: Clean Architecture é ideal para sistemas complexos e longevos. Para CRUDs simples ou protótipos, a sobrecarga de abstrações pode não valer a pena.
- Mantenha as interfaces enxutas: cada interface deve refletir exatamente o que a camada interna precisa, nada mais.
A regra de dependência é o pilar que sustenta toda a arquitetura limpa. Ela garante que as regras de negócio permaneçam imunes às mudanças tecnológicas. Quando você isola o que é essencial do que é acidental, o software se torna adaptável, testável e compreensível — exatamente o que buscamos como arquitetos.
Referências
- The Clean Architecture (Robert C. Martin) — Artigo original de Uncle Bob apresentando os conceitos fundamentais da Clean Architecture.
- Clean Architecture: A Craftsman's Guide (Amazon) — Livro de Robert C. Martin que aprofunda todos os princípios, incluindo a regra de dependência.
- Hexagonal Architecture (Alistair Cockburn) — Artigo seminal sobre Ports & Adapters, base conceitual para a Clean Architecture.
- The Dependency Inversion Principle (Robert C. Martin) — Artigo clássico sobre o DIP, princípio que fundamenta a regra de dependência.
- Clean Architecture .NET (Microsoft Docs) — Documentação oficial da Microsoft com exemplos práticos de Clean Architecture em .NET.
- Onion Architecture (Jeffrey Palermo) — Série de artigos que introduziu a Onion Architecture, precursora direta da Clean Architecture.