Pular para o conteúdo

Diagrama de clean architecture: camadas, regra de dependência e um exemplo completo

A clean architecture de Robert C. Martin como diagrama que você pode montar: as quatro camadas, a regra de dependência e um exemplo PlaceOrder com código e pastas.

· Fundador e desenvolvedor

Publicado em

Quatro pacotes UML em fila: frameworks and drivers, interface adapters, use cases e entities. O Web framework chama OrderController, que chama PlaceOrder, que cria Order. OrderPresenter e PostgresOrderRepository implementam interfaces da camada de use cases.

A maioria dos engenheiros já viu a imagem da clean architecture: quatro anéis coloridos com setas apontando para o centro. Bem menos gente sabe dizer o que vai em cada anel para uma funcionalidade real, para onde os imports devem apontar ou onde fica o SQL.

A lacuna aparece no code review: uma pasta domain que importa o ORM, um use case que devolve um código de status HTTP, cinco mappers entre um formulário e uma tabela.

Este post transforma os círculos de Robert C. Martin em um diagrama de componentes para um use case, fazer um pedido: as camadas, a regra de dependência, código e pastas, travessias de fronteira, clean versus hexagonal e onion, erros, quando é exagero e como desenhar.

As quatro camadas

Martin descreveu a clean architecture em um post de blog de 2012, The Clean Architecture, e a ampliou em seu livro de 2017, Clean Architecture. Ele a apresentou como uma integração de ideias anteriores, incluindo a arquitetura hexagonal e a onion. Do centro para fora:

Camada O que contém No exemplo Muda quando
Entities Regras de negócio da empresa, reutilizáveis entre aplicações Order e sua regra de que um pedido precisa de pelo menos uma linha Uma regra de negócio muda
Use cases Regras de negócio da aplicação, um fluxo cada PlaceOrder, suas duas interfaces, modelos de requisição e de resposta O fluxo muda
Interface adapters Controllers, presenters, gateways e todo o SQL OrderController, OrderPresenter, PostgresOrderRepository A API ou o esquema muda
Frameworks and drivers Ferramentas mais código de ligação: framework web, banco de dados, drivers O framework HTTP, PostgreSQL, pg, main.ts Você troca uma ferramenta

Os círculos são esquemáticos; Martin diz que você pode precisar de mais de quatro. A regra que os conecta nunca muda.

A regra de dependência

Nas palavras de Martin, «as dependências do código-fonte só podem apontar para dentro». O código de um círculo interno nunca menciona uma função, classe ou variável declarada em um círculo externo, nem usa seus formatos de dados. Para este exemplo:

  • entities não importa nada das outras camadas.
  • use-cases importa apenas entities: sem framework web, ORM ou driver.
  • Os adapters importam para dentro, mais a biblioteca que adaptam.
  • Só o main.ts enxerga tudo.

A regra cobre o código-fonte, não as chamadas em tempo de execução: em execução, PlaceOrder chama para fora, o presenter e o banco de dados. Martin resolve isso com o princípio da inversão de dependência: o use case chama uma interface que pertence a ele, e uma classe externa a implementa. A chamada sai; a dependência aponta para dentro.

Uma dependência de fato aponta para fora: o adapter importa o driver do banco de dados. Isso é proposital. Martin coloca todo o SQL na camada de adapters, e nenhum código mais interno «deve saber absolutamente nada sobre o banco de dados».

Faça a regra valer no CI com a verificação onionArchitecture() do ArchUnit na JVM, com o dependency-cruiser para TypeScript ou com o contrato layers do import-linter para Python.

Exemplo completo: fazer um pedido

Uma requisição POST /orders segue este caminho:

  1. O framework web a roteia para OrderController.
  2. O controller converte o corpo em um PlaceOrderRequest, um objeto simples que pertence à camada de use cases, cria um OrderPresenter e chama PlaceOrder.execute().
  3. PlaceOrder deixa Order.place() aplicar as regras de negócio, salva por meio de OrderRepository e passa um PlaceOrderResponse para PlaceOrderOutput.
  4. OrderPresenter implementa essa interface e monta um view model: status 201, um cabeçalho Location e um corpo JSON.
  5. O controller devolve o view model, e o framework o escreve.

A camada de use cases, com as duas interfaces que pertencem a ela:

// src/orders/use-cases/place-order.ts
import { Order } from '../entities/order';

export interface OrderRepository {             // implemented by an adapter
  nextId(): Promise<string>;
  save(order: Order): Promise<void>;
}
export interface PlaceOrderOutput {            // implemented by the presenter
  present(response: PlaceOrderResponse): void;
}
export type PlaceOrderRequest = {
  customerId: string;
  lines: { sku: string; quantity: number }[];
};
export type PlaceOrderResponse = { orderId: string; itemCount: number };

export class PlaceOrder {
  constructor(private readonly orders: OrderRepository) {}

  async execute(request: PlaceOrderRequest, output: PlaceOrderOutput): Promise<void> {
    const order = Order.place(await this.orders.nextId(), request.customerId, request.lines);
    await this.orders.save(order);
    output.present({ orderId: order.id, itemCount: order.itemCount() });
  }
}

Ela importa apenas a entity. Order.place() lança um erro quando um pedido não tem linhas ou tem quantidade abaixo de um, e o arquivo da entity não importa nada. O adapter importa para dentro, mais o driver:

// src/orders/adapters/postgres/postgres-order-repository.ts
import { randomUUID } from 'node:crypto';
import type { Pool } from 'pg';                         // adapters may import the driver
import type { Order } from '../../entities/order';
import type { OrderRepository } from '../../use-cases/place-order';

export class PostgresOrderRepository implements OrderRepository {
  constructor(private readonly pool: Pool) {}

  async nextId(): Promise<string> { return randomUUID(); }

  async save(order: Order): Promise<void> {
    await this.pool.query('INSERT INTO orders (id, customer_id, lines) VALUES ($1, $2, $3)',
      [order.id, order.customerId, JSON.stringify(order.lines)]);
  }
}

Só o composition root conhece cada classe concreta:

// src/main.ts
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const controller = new OrderController(new PlaceOrder(new PostgresOrderRepository(pool)));

app.post('/orders', async (req, res) => {
  const view = await controller.post(req.body);        // the presenter's view model
  res.status(view.status).set(view.headers).json(view.body);
});

Um teste unitário de PlaceOrder precisa apenas de um repositório em memória e de um presenter que registre sua entrada: sem servidor, sem banco de dados.

Estrutura de pastas

src/
  orders/
    entities/
      order.ts                        Order and its rules
    use-cases/
      place-order.ts                  PlaceOrder, both interfaces, request and response models
    adapters/
      http/
        order-controller.ts
        order-presenter.ts
      postgres/
        postgres-order-repository.ts
  main.ts                             composition root: builds and wires everything

Agrupe por funcionalidade e depois por camada. O argumento da screaming architecture de Martin: as pastas de nível superior devem dizer o que o sistema faz (orders, payments), não qual framework ele usa. O capítulo de Simon Brown no mesmo livro separa organização de encapsulamento: pastas organizam o código, enquanto regras de acesso ou uma verificação de build o fazem valer.

Atravessando fronteiras

Cada travessia de fronteira carrega um tipo que pertence a uma camada:

Travessia O que trafega Tipo pertence a Mecanismo
Framework → controller Requisição HTTP Framework Código de ligação em main.ts
Controller → use case PlaceOrderRequest Use cases Chamada direta
Use case → presenter PlaceOrderResponse Use cases Interface PlaceOrderOutput
Use case → banco de dados Order Entities Interface OrderRepository
Presenter → framework View model Adapters Devolvido pelo controller

Três hábitos mantêm essas travessias limpas:

  • Modelos de requisição e resposta simples. Martin é direto: «Não queremos trapacear e passar entities ou linhas do banco de dados». O presenter recebe PlaceOrderResponse, nunca Order, então renomear um campo da entity não mexe na API. Repositórios são uma exceção comum: muitas bases de código deixam que aceitem entities, o que ainda aponta para dentro. Times mais rigorosos mapeiam para um registro de persistência dentro do adapter.
  • Uma interface em cada chamada para fora, pertencente à camada interna e nomeada pelo que o use case precisa, não pela tecnologia.
  • Um único composition root. Mark Seemann define o composition root como «um local (de preferência) único em uma aplicação onde os módulos são compostos», perto do ponto de entrada. Martin chama main de «o detalhe supremo». Todo o resto recebe dependências por construtores.

A figura de Martin também tem uma interface Use Case Input Port na frente do interactor. Com um controller e uma implementação, chamar a classe diretamente é uma simplificação comum.

Clean versus hexagonal versus onion

Hexagonal (ports and adapters) Onion Clean
Texto de referência Alistair Cockburn, artigo de 2005 Jeffrey Palermo, série de blog de 2008 Robert C. Martin, post de blog de 2012, livro de 2017
Imagem Aplicação em um hexágono, adapters fora Anéis em volta do modelo de domínio Quatro círculos concêntricos
Estrutura interna Não prescrita Modelo de domínio, serviços de domínio, serviços de aplicação Entities, use cases
Implementação do repositório Adapter secundário (dirigido) Infraestrutura, anel externo Interface adapters
Regra central O código interno não deve vazar para fora; conversa por ports Todo acoplamento aponta para o centro As dependências do código-fonte apontam para dentro

As três colocam as regras de negócio no meio e fazem as bordas dependerem delas. Diferem em quanta estrutura prescrevem por dentro: o artigo original de Cockburn separa apenas o interior do exterior, a onion architecture de Palermo nomeia anéis em volta do modelo de domínio, e a clean architecture acrescenta presenters e modelos de requisição e resposta. O diagrama de arquitetura hexagonal percorre um exemplo de pedido parecido em termos de ports and adapters.

Erros comuns

  • Tipos de framework nas entities. Decorators de ORM ou atributos de serialização em Order prendem a camada mais interna à mais externa. Mapeie no adapter.
  • Um mapper para tudo. Seis cópias dos mesmos campos para um formulário que salva uma linha. Mapeie só onde as formas diferem.
  • Camadas sem comportamento. Entities de getters e setters, o modelo de domínio anêmico que Martin Fowler descreveu em 2003, e use cases que só chamam save().
  • Interfaces em todo lugar. Elas pertencem às fronteiras, não a cada classe.
  • HTTP no use case. Códigos de status pertencem ao presenter.

Quando é exagero

Palermo escreveu que a onion architecture «não é apropriada para sites pequenos», e o capítulo de Martin sobre fronteiras parciais abre com «Fronteiras arquiteturais completas são caras».

Situação Opção melhor
Serviço CRUD com poucas regras Uma aplicação em camadas enxuta; mantenha as poucas regras fora dos controllers
Protótipo ou ferramenta de vida curta Sem camadas
Uma dependência volátil, como um provedor de pagamento Uma interface e um adapter para ela
Produto de vida longa com regras reais e vários pontos de entrada As quatro camadas

Esse capítulo também descreve fronteiras parciais mais baratas; promova uma quando uma segunda implementação ou um teste precisar dela.

O diagrama de clean architecture

Quatro pacotes UML em fila: frameworks and drivers, interface adapters, use cases e entities. O Web framework chama OrderController, que chama PlaceOrder, que cria Order. OrderPresenter e PostgresOrderRepository implementam interfaces da camada de use cases.

20 componentes · 9 conexões

Abrir no Archboard
Conteúdo do diagrama
  • Frameworks and drivers
  • Interface adapters
  • Use cases
  • Entities
  • Web framework
  • PostgreSQL
  • OrderPresenter
  • OrderController
  • PostgresOrderRepository
  • PlaceOrderOutput
  • PlaceOrder
  • OrderRepository
  • Order
  • POST /orders
  • execute(request)
  • Order.place()
  • reads view model
  • present(response)
  • save(order)
  • implements
  • SQL via pg driver
Um use case, PlaceOrder, traçado pelas quatro camadas da clean architecture, a mais externa à esquerda. Nenhuma seta aponta para fora: o presenter e o repositório Postgres implementam interfaces da camada de use cases; o controle sai, as dependências apontam para dentro.

Como ler:

  • Quatro pacotes UML, o mais externo à esquerda. Ir para a direita é ir para dentro.
  • Setas contínuas são chamadas e imports. Três atravessam uma fronteira em direção às entities. As outras três ficam dentro de uma camada: present(response) e save(order) chamam as duas interfaces que pertencem à camada de use cases.
  • Setas tracejadas com triângulos vazados significam implementa. O presenter e o repositório apontam para dentro, para essas interfaces, enquanto o controle flui para fora.
  • A linha pontilhada é SQL pelo driver, e ela começa em um adapter.
  • As notas e o bloco Markdown dizem onde fica o main.ts, o que a entity impõe e o que cada estilo de linha significa.

Desenhando diagramas de clean architecture com clareza

  • Desenhe um use case, não os anéis. Trace uma funcionalidade real com nomes de classe reais.
  • Coloque cada interface na camada a que ela pertence. OrderRepository fica ao lado de PlaceOrder, não ao lado da sua implementação.
  • Use dois estilos de seta: contínua para chamadas, tracejada com triângulo vazado para implementa, como na realização do UML. O exemplo de diagrama de classes UML cobre a notação.
  • Dê uma única direção a «para dentro» na página. Uma seta voltando é uma violação ou uma chamada em execução que precisa de rótulo.
  • Rotule as setas com o método e o modelo que atravessa.
  • Deixe de fora campos de DTO, internos de framework e o contêiner de DI.

Para o sistema ao redor desta visão no nível do código, veja o exemplo de diagrama do modelo C4 e o guia para desenhar diagramas de arquitetura de software.

Monte no Archboard

  1. Entre com Google ou GitHub e crie um diagrama.
  2. Adicione quatro nós Pacote do pack UML em fila: Frameworks and drivers, Interface adapters, Use cases e Entities, com destaques azul, verde, vermelho e âmbar.
  3. Em Frameworks and drivers, adicione um Componente do pack UML chamado Web framework e um nó Postgres do pack Dados chamado PostgreSQL.
  4. Em Interface adapters, adicione dois nós Componente, OrderPresenter e OrderController, e uma Classe chamada PostgresOrderRepository.
  5. Em Use cases, adicione uma Classe chamada PlaceOrder entre dois nós Interface, PlaceOrderOutput e OrderRepository.
  6. Em Entities, adicione uma Classe chamada Order com seus campos e métodos.
  7. Desenhe seis arestas contínuas rotuladas para as chamadas, duas arestas tracejadas com pontas de triângulo para implementa e uma aresta pontilhada de PostgresOrderRepository para PostgreSQL.
  8. Adicione um bloco Markdown do pack Básico com a regra e os estilos de linha, e notas de Texto do pack Básico para cada camada.

Um fluxograma Mermaid das suas camadas vira um primeiro rascunho com a ferramenta de Mermaid para diagrama editável. O Free permite 15 nós de diagrama por diagrama, então trace um use case por diagrama.

O exemplo usa 14 nós de diagrama e fica dentro do plano Free.

Perguntas frequentes

Quais são as quatro camadas da clean architecture? Entities (regras de negócio da empresa), use cases (regras de negócio da aplicação), interface adapters (controllers, presenters e gateways) e frameworks and drivers (o framework web, o banco de dados e outras ferramentas). Martin diz que os círculos são esquemáticos: você pode precisar de mais de quatro.

O que é a regra de dependência? As dependências do código-fonte apontam apenas para dentro. O código de uma camada interna nunca nomeia nada declarado em uma camada externa, então os use cases não sabem nada sobre controllers, SQL ou o framework web.

Qual é a diferença entre clean architecture e arquitetura hexagonal? As duas mantêm as regras de negócio no meio e fazem os adapters dependerem delas. A arquitetura hexagonal separa apenas o interior do exterior, por meio de ports. A clean architecture também divide o interior em entities e use cases e nomeia presenters e modelos de requisição e resposta.

Onde fica a interface do repositório na clean architecture? Na camada de use cases, ao lado do use case que precisa dela. A implementação, com o SQL, pertence à camada de interface adapters, e o composition root a injeta na inicialização.

A clean architecture é exagero para projetos pequenos? Muitas vezes. Para serviços CRUD, protótipos e ferramentas de vida curta, as interfaces e os mapeamentos extras custam mais do que devolvem. Adicione interfaces para as dependências que você espera que mudem e faça as camadas crescerem quando surgirem regras de negócio reais.

Comece grátis — um assento, sem cartão de crédito.

Artigos relacionados

← Voltar ao blog

Abra um canvas em um minuto

Entre com Google ou GitHub. Grátis para um assento. Faça upgrade quando a equipe entrar.