Arquitetura Hexagonal com Golang

Vinny Nordi
Avenue Tech
Published in
7 min readNov 4, 2021

Todo time de desenvolvimento, com certeza, já teve a seguinte dúvida: Qual melhor maneira de organizar meu código?

Trabalhar como desenvolvedor de software é estar em busca de, cada dia que passa, evoluir a plataforma de maneira sustentável, com intuito de facilitar futuras implementações de negócio.

Uma das técnicas utilizadas para alcançar um código bem estruturado, facilmente testável, e de simples manuteção, encontra-se na Arquitetura Hexagonal ou Padrão de Portas e Adaptadores.

Mas antes de entrar em detalhes de implementação (com Golang), quais são os conceitos por trás desse padrão?

Arquitetura Hexagonal

A Arquitetura Hexagonal, proposta por Alistair Cockburn, permite criar uma aplicação onde a lógica de negócio está em um núcleo (core), não dependente de sistemas externos, facilitando, assim, testes de regressão. A arquitetura foi pensada de forma que adaptadores (adapters) possam ser “plugados” (dependency injection) no sistema a partir de portas, não afetando a lógica de negócio que foi definida no núcleo do sistema.

Motivação

A maior tentação de um desenvolvedor quando criando sua aplicação, é mesclar a regra de negócio nas camadas de interface de usuário. Tal atitude pode causas diversas complicações:

  • Caso haja necessidade de realizar a mudança da camada de APIs, por uma camada GRPC, a lógica de negócio terá que ser transportada para a nova camada TPC;
  • Caso a mesma regra de negócio possa ser executada à partir de um batch job, parte do negócio também terá que ser portado para a nova execução;
  • e assim por diante…

Mesmo que haja um esforço muito grande de uma empresa em evitar que os problemas acima ocorram, é muito difícil um monitoramento, ao longo de vários anos, que evite esse problema.

Uma situação similar ocorre na outra ponta do sistema, quando a lógica de negócio fica atrelada a algum banco de dados, ou APIs externas. No caso de uma posterior mudança de parceiro que se consome as APIs, ou mudança de uma banco de dados (por exemplo, migrar do MySQL para o Postgres), torna-se, muitas vezes, inviável a portabilidade, já que a lógica e serviços externos se entrelaçam no código.

Novamente, por maior que seja o esforço em se manter as camadas separadas, dificilmente existirá um monitoramento que evite, ou sinalize, quando isso ocorre.

Estrutura

Pode-se dizer que a Arquitetura Hexagonal é dividida em quatro grandes partes e conceitos: núcleo, adaptadores, portas e atores.

Representação da arquitetura hexagonal

Núcleo

É a parte central do sistema. Consiste nas entidades e regras de negócio. O núcleo é independente da infraestrutura da aplicação, e facilmente testável em isolamento.

Nessa arquitetura, o núcleo não sabe da existência de nenhum componente que o envolve. Em resumo, é o coração da aplicação.

Atores

Os atores são tudo o que quer interagir com o núcleo, sejam pessoas, banco de dados ou mesmo outros sistemas. Eles podem ser classificados em atores primários e secundários:

  • Atores primários: são aqueles que iniciam a comunicação com o núcleo. Exemplos: uma outra aplicação, um usuário
  • Atores secundários: são aqueles que esperam que o núcleo inicie a comunicação com eles. Exemplos: banco de dados, sistemas de mensageira, outras aplicações

Os atores e o núcleo conversam em diferentes linguagens. Um caso é de uma aplicação externa que envia uma requisição http para executar um serviço do núcleo, que não entende o que o http significa. Outro caso é quando o núcleo, que é agnóstico a tecnologia, quer enviar uma mensagem para algum serviço, através de um sistema de mensageira.

Portanto, essa interface entre o núcleo e os atores, deve ser realizada por alguma entidade que entenda como converter as informações que o núcleo entende, para as informações num formato que os atores entendam.

Para isso, existem os conceitos de adaptadores e portas.

Portas

As portas são as interfaces, que pertencem ao núcleo, e definem como a comunicação entre os atores e o núcleo devem ser realizadas.

Para cada tipo de ator, existe um tipo de porta diferente:

  • Portas para atores primários: são as definições dos casos de uso que o núcleo implementa e expõe, para ser consumido por atores externos
  • Portas para atores secundários: são as definições de ações que o ator secundário tem que implementar

Adaptadores

Os adaptadores são os responsáveis pela tradução das informações entre núcleo e os atores. Eles também são classificados entre primários e secundários, seguindo a mesma ideia de atores e portas.

Injeção de dependência

É o momento onde os adaptadores são plugados com suas respectivas portas. Isso podem ser feito quando a aplicação se inicia, e permite que, por exemplo, sejam escolhidos entre um banco de dados em memória, para testes, ou Postgres para aplicação em produção.

Ao que interessa

A situação abaixo é uma simples implementação de uma lista to-do, utilizando os conceitos de arquitetura hexagonal. A ideia é criar algumas APIs que consumirão do núcleo da aplicação, e o mesmo armazenando e recuperando informações de diferentes bancos de dados: MySQL e MongoDB.

TL;DR: código no github.

Estrutura do projeto

O projeto consiste em três principais pastas: helpers, internal e migrations. Helpers e Migrations contém arquivos suporte, tanto para criação das tabelas no banco de dados, quanto para inicialização da conexão com os banco de dados, ou mesmo transformações necessárias.

Dentro da pasta internal está toda a lógica de negócio, além das portas e adaptadores.

Na pasta core, temos todo o núcleo do sistema, contendo as regras de negócio, os domínios e as portas.

Os domínios (domain) são as representações das entidades importantes para o sistema, como, por exemplo, a estrutura ToDo.

As portas (ports) contém as assinaturas dos métodos que são utilizados pelos adaptadores, a fim de realizar as operações desejadas.

Para o caso de nosso projeto, temos portas para os casos de uso (primárias)

e para os repositórios (secundárias)

Os casos de uso (use cases) representam a implementação da lógica de negócio, independente do tipo de banco de dados utilizado, ou de como o serviço será exposto (http ou grpc, por exemplo). Nesse momento, podemos observar a utilização das portas secundárias, que são responsáveis por exportar os repositórios.

Observe que, na linha 15, é utilizada apenas a definição exposta pelas portas. Portanto, os casos de uso não tem visibilidade (e nem devem!) do banco de dados utilizado. Isso torna a implementação transparente, facilitando uma alteração de um banco de dados MySQL para o MongoDB, por exemplo. Nesse caso, nenhuma alteração no código do caso de uso deverá ser feita. Apenas a injeção de dependência do adaptador, que veremos posteriormente, deverá ser alterada.

Por fim, em handlers/todo e repositories/todo são implementados, respectivamente, os adaptadores para comunicação http e comunicação com banco de dados.

Handler

No código acima, é possível visualizar a separação entre casos de uso e handler. A linha 11 apresenta a estrutura do handler sendo composta pela porta do caso de uso ToDo. Nesse sentido, caso seja necessário a implementação de um adaptador que receberá requisições grpc, os casos de uso ficarão intactos, uma vez que estão isolados.

Outro ponto positivo a ser observado, é o fato de, caso a lógica de negócios se altere, adaptador handler não será afetado.

MySQL

Mongo

Acima, as implementações da comunicação com os bancos de dados MySQL e MongoDB. Em momento nenhum existe alguma menção sobre outras partes do código, como os casos de uso. Pura e simplesmente a implementação das ações e conversões necessárias para que se repasse o domínio ToDo, ao invés das estruturas específicas para cada banco, para os casos de uso.

Por fim, e não menos importante, o arquivo principal do nosso projeto, que contém todas as inicializações necessárias, e responsável por “escolher” qual repositório será utilizado, dentro do caso de uso ToDo.

O arquivo main é o orquestrador. Ele verifica qual repositório o serviço deve utilizar, levando em consideração as configurações iniciais (linhas 33–37. Além disso, ele inicia o caso de uso ToDo com o repositório escolhido (linha 39). E na linha 43, pode-se notar a inicialização do adaptador handler, que injeta o caso de uso em seu contexto.

Conclusões

A arquitetura hexagonal nos trás várias vantagens como separação de dependências, foco na lógica de negócio, uma vez que ela pode ser implementada de maneira totalmente independente do resto do sistema, facilitar alteração da infraestrutura (como mudança de um banco de dados, ou da camada de entrada), e até mesmo permite que testes em isolamento sejam realizados de maneira muito simples.

Contudo, existem os pontos negativos. Para serviços pequenos, pode ser um grande desperdício de tempo separar a sua aplicação dessa maneira, pois a demanda de tempo é grande. Outro ponto negativo também é o possível aumento do tempo de resposta, pelo sistema ser separado em tantas e diferentes camadas. Em sistemas de altíssima performance, isso pode custar algum tempo valioso.

--

--

Vinny Nordi
Avenue Tech

Lutando contra o lado sombrio dos micro-serviços