Rumo ao Hexa? A Arquitetura Hexagonal (Parte 1)

Demis Gomes
6 min readJul 9, 2020

--

Photo by James Hon on Unsplash

Arquitetura de software sempre é um assunto que dá muito pano pra manga. Para alinhar conceitos rígidos à entregas apertadas com clientes que mudam requisitos todos os dias é preciso ter muito jogo de cintura.

Alguns conceitos já estão batidos como “use camadas de controle, serviço, domínio e repositório”. Um projeto com uma lógica toda implementada na camada web/apresentação por exemplo já dá calafrios em devs mais experientes ou em líderes de projeto que conduzem uma entrevista. Porém, existem mais padrões que podem deixar o código “mais limpo”.

Um destes padrões é chamado de “ports and adapters”, mas fica mais legal quando falamos a sua variação, a “arquitetura hexagonal”, criada pelo Alistair Cockburn em 2005.

Então, o que seria essa arquitetura?

Segundo o criador, a arquitetura hexagonal:

“Permite que uma aplicação seja igualmente controlada por usuários, programas, testes automatizados, batch scripts e seja desenvolvida e testada isoladamente de seus dispositivos e banco de dados em tempo de execução”

Desse modo, a arquitetura preza por um desacoplamento bem definido entre camadas com um grande foco na facilidade de aplicar testes automatizados. Camadas mais externas devem apenas depender de camadas mais internas. Até agora, porém, nenhuma novidade certo?

A grande diferença desta arquitetura para a que falamos no começo (Controller -> Service -> Repository) consiste de três pontos importantes:

  • o uso de ports e adapters;
  • a definição explícita de use cases;
  • abordagem externa->interna e não top->down

Não entendeu? Calma! Vamos explicar tudinho ;)

Exemplo de arquitetura hexagonal. Fonte: reflectoring.io

Ports e Adapters

Imagine a aplicação core como uma grande caixa preta. Ela permite o acesso de um sistema externo (camada web, filas, UI, entre outros) através de portas de entrada (input ports) e se conecta à outros sistemas externos (integrações, persistência) por portas de saída (output ports). Portanto, a aplicação core pode ser testada de modo isolado sem dependências externas. Os adapters são, justamente, os sistemas externos que compõem a aplicação final.

Aqui reside o desenho mais conhecido da arquitetura hexagonal: do lado esquerdo, as entradas do sistema (camada web, interface gráfica, filas), enquanto que do lado direito aparecem as integrações com outros serviços e persistência.

Esta figura também possui um comportamento sutil mas importante. As setas que possuem conectores preenchidos em preto dependem dos componentes ligados (ex: Web adapter depende de input port). Por outro lado, as setas com conectores preenchidos em branco implementam as portas associadas (ex: PersistenceAdapter implementa a output port).

Portanto, embora os adapters se relacionem com as ports, diferenciam-se na responsabilidade. Adapters de entrada apenas chamam uma input port que é implementada por um use case. Um use case não precisa conhecer como o sistema externo recuperou a informação ou persistiu o dado, mas chama uma output port que é implementada por um adapter de saída.

Use Cases

Embora o nome relembre definições de um processo árduo e cansativo, os use cases têm a missão de definir o fluxo das funcionalidades da aplicação na linguagem do usuário.

Por exemplo, imagine uma aplicação que aplique um reconhecimento de imagens de língua de sinais. Um possível use case seria: “eu como usuário, dado uma imagem, gostaria que o sistema indicasse qual o sinal que obtém a maior similaridade e salvasse o resultado em um arquivo .csv”. O código do use case receberia a imagem, aplicaria o algoritmo de reconhecimento definido no domínio da aplicação e teria uma output port para escrever o arquivo csv. Note que, com o domínio bem definido, a entrada da aplicação poderia vir de qualquer adapter e poderia ser persistida não só em um CSV, mas em qualquer outra estrutura de persistência.

Os use cases geralmente seguem uma sequência de passos que são realizados por outros componentes e, por isso, se assemelham ao nível do usuário. Algumas implementações validam os dados no use case e outras no domínio, ou seja, varia da interpretação de cada um. No geral, ao invés de termos um arquivo Service para as regras de negócio da aplicação, temos um arquivo para cada use case, o que mantém os princípios de responsabilidade única do SOLID. Por exemplo, ao invés de um Service ter um método para cadastrar e outro para consultar, o comportamento é dividido em dois use cases: RegisterUseCase e FindUseCase.

Domínio

Nesta camada você pode ter apenas entidades (como está na imagem acima) ou comportamentos. No exemplo anterior do reconhecimento de imagens, o domínio possui a lógica mais importante que aplica o algoritmo de reconhecimento, ou seja, ele deve estar no core da aplicação. Regras de negócio desse tipo como cálculos, previsões, simulações, mudança de status de um processamento, entre outros, podem ser mantidas no domínio.

O ideal é que o domínio esteja desacoplado de outras dependências do sistema. Desse modo, você pode executar uma simulação tranquilamente chamando essa lógica via CLI, web, lib, e etc. Em microsserviços, o desacoplamento é fundamental em pacotes commons que são usados por diferentes serviços do sistema.

De fora para dentro

Como visto na figura, os componentes não são dispostos em uma estrutura linear top-down, mas sim da esquerda para direita envolvendo um núcleo de uma estrutura hexagonal composta por use cases e domínio. Quanto mais perto do centro, mais próximo do domínio e da lógica mais importante da aplicação. Algumas pessoas chamam isso de onion architecture (Arquitetura cebola) devido à quanto mais retiramos camadas, mais chegamos perto do núcleo da cebola.

Se você já leu ou conhece alguns conceitos abordados pelo Robert C. Martin (conhecido como Uncle Bob), deve ter relacionado essa arquitetura hexagonal com a Clean Architecture. De fato, a Clean Architecture criada em 2012 é uma variação da arquitetura hexagonal. O princípio básico de camadas mais externas dependerem de camadas mais internas, use cases, interface adapters e domínio no core casam com o que vimos até aqui.

Clean Architecture (Fonte: Medium)

Algumas pessoas como a Camila Campos, uma das referências de arquitetura hexagonal no país, tratam a Clean Architecture e a hexagonal como sinônimos. Deixo aqui um vídeo sobre uma das palestras que ela conduziu para você observar como ela aborda os dois temas.

Uma abordagem que a Clean Architecture propõe é o fluxo de controle entre Controller e Presenter. Imagine a situação: uma UI solicita o processamento para o core da aplicação via input port. Quando a aplicação retorna algo para a UI, ela deveria optar por enviar essa informação por uma output port para manter a definição de que input ports servem apenas para receber informações de fora.

Fonte: Crosp.net

Neste caso, como a imagem acima aborda, um Use Case Interactor pode receber a informação de uma input port e enviar o resultado por uma output port. Do lado dos adapters, o Controller apenas envia a informação, enquanto que o Presenter recebe da aplicação core.

Embora este padrão não esteja definido pela arquitetura hexagonal, ela acaba se encaixando nos conceitos definidos por ela. Porém, ela ainda levanta alguns questionamentos quanto à necessidade de seguir à risca a definição dos elementos de input e output ports. No exemplo que vamos abordar no próximo post, não implementaremos esse padrão, ou seja, o Controller vai enviar e receber a resposta do use case pela input port.

Como principais benefícios, acredito que a arquitetura se alinha bastante aos conceitos do SOLID e busca manter a lógica no domínio o máximo que puder. Como tradeoff, o uso de mappers para converter objetos de domínio para persistência e vice-versa pode ocasionar uma carga de trabalho que pode se tornar cansativa e deve ser ponderada.

Achou interessante? Não ficou satisfeito? Então que tal dar uma olhada na parte 2 onde abordamos as diferenças de uma aplicação na abordagem tradicional para uma hexagonal? O exemplo será feito em Kotlin + SpringBoot. Caso você queira dar uma olhada em como seria o processo em NodeJS, recomendo o tutorial do Guerreiro Programador.

Até mais pessoal! Abraço e fiquem bem.

--

--

Demis Gomes

Mestre em Ciência da Computação. Atualmente é Consultor de Desenvolvimento. Gosta também de um bom futebol, Fórmula 1, Star Wars, brega e doce de leite.