Diagrama de arquitetura hexagonal: portas e adaptadores com um exemplo

A arquitetura hexagonal com um diagrama: o núcleo, as portas de entrada e saída, os adaptadores primários e secundários, a regra de dependência e um exemplo.

· Fundador e desenvolvedor

Publicado em

Diagrama de arquitetura hexagonal: portas e adaptadores com um exemplo

A maioria das bases de código começa com o framework no centro: controllers chamam services, services chamam o ORM, e as regras de negócio vazam para os três. Testar uma regra de preço passa a exigir um servidor web e um banco de dados, e trocar o provedor de pagamentos significa mexer no código que decide quanto um pedido custa.

A arquitetura hexagonal, também chamada de portas e adaptadores, inverte isso. Este artigo explica a ideia e o vocabulário (portas, adaptadores primários e secundários, a regra de dependência), mostra um diagrama para um serviço de pedidos e discute quando o padrão compensa os arquivos a mais.

A ideia em uma frase

Coloque a lógica de negócio num núcleo que não sabe nada de HTTP, bancos de dados ou fornecedores, e conecte todo o resto a ele por meio de interfaces que pertencem ao núcleo.

Alistair Cockburn descreveu o padrão em 2005 no seu artigo original sobre arquitetura hexagonal. A intenção declarada é permitir que uma aplicação seja acionada igualmente por usuários, programas, testes automatizados ou scripts em lote, e que seja desenvolvida e testada isolada dos bancos de dados e dispositivos que vai usar. O formato de hexágono não significa nada além de deixar espaço para desenhar várias portas nos lados.

O vocabulário

Termo O que é Exemplo no serviço de pedidos
Núcleo Os casos de uso e o modelo de domínio PlaceOrder, Order, Money, regras de preço
Porta de entrada (primária) Uma interface que o núcleo oferece para fora PlaceOrder.execute(command)
Porta de saída (secundária) Uma interface de que o núcleo precisa lá de fora OrderRepository, PaymentGateway
Adaptador primário Transforma uma entrada externa em chamadas de porta Controller REST, CLI, consumidor de mensagens, teste
Adaptador secundário Implementa uma porta de saída com tecnologia real Repositório Postgres, cliente da API de pagamentos

A regra de dependência amarra tudo: as dependências do código-fonte apontam para dentro. Os adaptadores dependem das portas; as portas e o núcleo não dependem de nada fora do hexágono. Em tempo de execução o núcleo chama o gateway de pagamento, mas no código é o adaptador de pagamento que depende da interface do núcleo, e não o contrário.

Um exemplo completo

Fazer um pedido exige salvar o pedido e cobrar o cliente. No núcleo:

// ports, owned by the core
interface OrderRepository { save(order: Order): Promise<void> }
interface PaymentGateway { authorize(amount: Money, token: string): Promise<AuthResult> }

// use case
class PlaceOrder {
  constructor(private orders: OrderRepository, private payments: PaymentGateway) {}
  async execute(cmd: PlaceOrderCommand): Promise<OrderId> {
    const order = Order.create(cmd.items, cmd.customerId)   // domain rules
    const auth = await this.payments.authorize(order.total(), cmd.paymentToken)
    if (!auth.ok) throw new PaymentDeclined(auth.reason)
    await this.orders.save(order.markPaid(auth.id))
    return order.id
  }
}

Fora do núcleo, PostgresOrderRepository implementa OrderRepository com SQL, e PaymentApiGateway implementa PaymentGateway com chamadas HTTP, retries e mapeamento de erros. Um controller REST interpreta a requisição, monta um PlaceOrderCommand e chama o caso de uso. Um teste faz o mesmo com adaptadores em memória, sem servidor web nem banco de dados.

Mapeando para pastas

A estrutura é mais fácil de manter quando as pastas a dizem em voz alta:

src/
  core/
    domain/        Order, Money, pricing rules
    usecases/      PlaceOrder, CancelOrder
    ports/         OrderRepository, PaymentGateway
  adapters/
    http/          REST controllers (driving)
    postgres/      repository implementations (driven)
    payments/      payment API client (driven)
  main.ts          wiring: builds adapters and passes them to use cases

Adicione uma regra de lint ou um teste de arquitetura que quebre o build sempre que algo dentro de core/ importar de adapters/ ou de um pacote do framework. A regra é fácil de quebrar sem querer e barata de verificar automaticamente.

Erros comuns

  • Portas com a cara da tecnologia. OrderRepository.executeQuery(sql) é uma API de banco de dados com outro nome. Dê às portas nomes do que o núcleo precisa: save, findOpenOrdersFor(customer).
  • Objetos de domínio que são entidades do ORM. As anotações do framework de persistência arrastam o banco de dados para dentro do núcleo. Faça o mapeamento entre objetos de domínio e modelos de persistência no adaptador.
  • Adaptadores com regras de negócio. Um adaptador de pagamento que decide que reembolsos acima de 500 dólares precisam de aprovação tirou uma regra do núcleo. Limite os adaptadores a tradução e transporte.
  • Uma porta por classe. As portas existem para as fronteiras, não para cada colaborador dentro do núcleo.

O diagrama de arquitetura

Um hexágono representa o núcleo, com o caso de uso PlaceOrder, o modelo de domínio e três portas. Um controller REST e um driver de testes chamam a porta de entrada. O caso de uso chama as portas OrderRepository e PaymentGateway, implementadas por um adaptador Postgres e um de pagamentos.

14 componentes · 10 conexões

Abrir no Archboard
Conteúdo do diagrama
  • PlaceOrder use case
  • Domain model
  • PlaceOrder port
  • OrderRepository port
  • PaymentGateway port
  • REST controller
  • Test driver
  • Postgres adapter
  • Payment API adapter
  • Postgres
  • Payment provider
  • calls
  • implements
  • SQL
  • HTTPS
Um serviço de pedidos em arquitetura hexagonal. O núcleo contém o caso de uso PlaceOrder e o modelo de domínio; chama portas de saída que adaptadores implementam para o Postgres e um provedor de pagamentos, e é acionado por um controller REST ou um driver de testes via porta de entrada.

Como ler:

  • O hexágono é o núcleo da aplicação: o caso de uso PlaceOrder e o modelo de domínio, sem código de framework ou de fornecedor.
  • Esquerda, lado primário: um controller REST e um driver de testes chamam a porta de entrada PlaceOrder. Do ponto de vista do núcleo, eles são intercambiáveis.
  • Direita, lado secundário: o caso de uso chama duas portas de saída. As arestas tracejadas com ponta de seta vazada mostram o adaptador Postgres e o adaptador da API de pagamentos implementando-as, que é a direção da dependência do código.
  • Extrema direita: o banco Postgres real e o provedor de pagamentos, que só os adaptadores conhecem.
  • O bloco Markdown enuncia a regra de dependência, a única coisa que os revisores precisam checar.

Os testes ficam mais baratos

A maior parte do benefício aparece nos testes:

  • Os testes do núcleo usam adaptadores em memória e rodam em milissegundos. Regras de preço, transições de estado e tratamento de erros são testados sem I/O.
  • Os testes de adaptador verificam um adaptador contra a tecnologia real: o repositório Postgres contra um Postgres de verdade, o adaptador de pagamento contra o sandbox do provedor.
  • Alguns testes de ponta a ponta comprovam a ligação entre as partes.

Essa divisão é a pirâmide de testes na prática, e é por isso que os times adotam o padrão até em serviços pequenos.

Adicionando uma nova entrada

A porta de entrada se paga na primeira vez que aparece um segundo canal. Digamos que o negócio queira receber pedidos pela fila de mensagens de um parceiro, além da web. Você escreve um consumidor que lê uma mensagem, monta o mesmo PlaceOrderCommand e chama a mesma porta. Validação, preços e lógica de pagamento não mudam, e os testes deles também não. Sem a porta, o resultado comum é uma segunda cópia da lógica de pedidos dentro do consumidor, que se afasta da primeira em poucas versões.

Quando não vale a pena

  • CRUD sem regras. Se o serviço basicamente move dados entre o HTTP e uma tabela, as portas são só cerimônia.
  • Código de vida curta. Protótipos e scripts não pagam as interfaces extras.
  • Um único adaptador para sempre. Se você nunca vai trocar o banco de dados e já testa contra um banco real, uma porta de saída adiciona indireção com pouco retorno. Mesmo assim, mantenha o núcleo livre de código de framework.

A arquitetura hexagonal é parente próxima da arquitetura cebola e da clean architecture. Elas diferem no vocabulário das camadas, mas compartilham a regra de dependência.

O que colocar no diagrama

  • A fronteira do núcleo, desenhada como uma forma que envolva claramente só o domínio e os casos de uso.
  • As portas na fronteira, com os nomes que o núcleo dá a elas.
  • Os adaptadores do lado de fora, rotulados como primários ou secundários, com a tecnologia que encapsulam.
  • A direção da dependência dos adaptadores secundários, diferente da direção da chamada.

Para um sistema inteiro em vez de um único serviço, o exemplo de diagrama com o modelo C4 mostra como dar zoom de contêineres para componentes, e o diagrama de arquitetura de microsserviços mostra onde serviços como este se encaixam. O diagrama de integração com gateway de pagamento cobre o que acontece por trás do adaptador de pagamento.

Monte no Archboard

  1. Entre com Google ou GitHub e crie um diagrama.
  2. Adicione um Hexágono do pack Básico para o núcleo da aplicação, com um Texto como rótulo.
  3. Dentro dele, adicione nós Componente do pack Modelo C4 para o caso de uso e o modelo de domínio, e nós Componente na fronteira para as três portas.
  4. Adicione do lado de fora nós Contêiner para os adaptadores primários e secundários, além de Postgres do pack Dados e um Sistema externo para o provedor de pagamentos.
  5. Desenhe as arestas de implementação tracejadas com ponta de seta vazada e adicione um bloco Markdown do pack Básico com a regra de dependência.

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

Perguntas frequentes

Qual a diferença entre uma porta e um adaptador? Uma porta é uma interface que pertence ao núcleo. Um adaptador é o código que conecta essa interface a uma tecnologia específica, como um framework REST ou um banco de dados.

O que são adaptadores primários e secundários? Os adaptadores primários iniciam as interações com o núcleo, como controllers, consumidores e testes. Os adaptadores secundários são chamados pelo núcleo, como repositórios e clientes de API.

Arquitetura hexagonal é a mesma coisa que clean architecture? Elas compartilham a regra de dependência: as dependências apontam para o domínio. A clean architecture e a arquitetura cebola adicionam camadas com nome dentro do núcleo; a arquitetura hexagonal só distingue o lado de dentro do lado de fora.

Todo serviço precisa de arquitetura hexagonal? Não. Ela compensa onde há regras de negócio reais e mais de uma forma de acioná-las ou armazená-las. Em serviços CRUD finos, são basicamente arquivos a mais.

Onde ficam as transações? Normalmente no caso de uso ou numa porta de unit of work, para que o núcleo decida o que precisa ser confirmado junto e o adaptador decida como.

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.