Arquitetura Hexagonal Explicada: como Transformar seu Desenvolvimento de Software

Arquitetura Hexagonal Explicada: como Transformar seu Desenvolvimento de Software

Introdução
Ciclo de um projeto
Arquitetura vs Design de Software
Arquitetura Hexagonal
Hexagonal vs Clean vs Onion

Introdução

De forma geral, entende-se que a função de todo desenvolvedor é “resolver problemas utilizando o software como ferramenta”. Esta frase, apesar de simples, pode ser entendida a partir de duas vertentes: complexidade de negócio, envolvendo as regras para se resolver o problema; e a complexidade técnica, que é a tecnologia integrada ao negócio para fazê-lo funcionar.

Muitas vezes, porém, o que observamos são aplicações tão intrinsecamente integradas de forma que não é mais possível destacar o que é complexidade de negócio e o que é a complexidade técnica.

Considere o seguinte cenário: a Empresa X deseja rastrear eventos de usuários em seu aplicativo, como ‘account_created’, ‘home_viewed’, e ‘payment_finished’, e enviá-los para uma plataforma de CRM, Empresa Y, para construir perfis detalhados e aprimorar estratégias de marketing. Para isso, utilizam o SDK da Empresa Y com o método ‘send_event’, implementado em cada ponto do sistema onde ocorre um evento. Dois meses depois, com o aumento dos custos pelo uso do sistema da Empresa Y, a Empresa X opta por migrar para a Empresa Z. A substituição do SDK de Y pelo de Z seria um processo simples nesse caso?

Podemos facilmente concluir que não. O código de Y ficou acoplado por todo o sistema, “sujando” a complexidade de negócio. Substituir a sua utilização seria um processo árduo e demorado.

A arquitetura hexagonal, também chamada de “Ports and Adapters”, vem exatamente para impedir que isso aconteça. Nesta linha, podemos exemplificar alguns pontos importantes sobre uma arquitetura bem feita:

Crescimento sustentável: o software precisa ser escalável e possível de ser melhorado de forma simples, de forma que ao longo do tempo ele pague o seu próprio investimento;
O Software deve ser desenhado por nós e não pelo framework – devemos mitigar o acoplamento entre complexidade técnica e de negócio;
Possível de ser substituída sem muitos esforços

Em outras palavras, podemos entender a arquitetura como o futuro de um software.

Ciclo de um projeto

Ainda sim, muitas vezes, seja por falta de conhecimento sobre arquitetura, ou mudanças complexas de mercado, vemos um software crescer de uma maneira bem pouco sustentável. A seguir exemplifico o ciclo de vida de muitos projetos hoje em dia.

Fase 1:
Nesta fase, o projeto é iniciado. Implementa-se o banco, cadastros, autenticação, servidor, views, controllers, etc.

Fase 2:
Adiciona-se regras de negócio no cadastro, implementa-se uma ACL para autorização, adicionam-se logs, criação e consumo de APIs.

Fase 3:
Os acessos aumentam muito, escala-se horizontalmente a aplicação (upgrade de hardware), começamos a trabalhar com cache, consumir apis de terceiros e implementar regras de neógio de parceiros.

Fase 4:
Mais acessos, mais upgrades, mais consultas ao banco que começam a gerar gargalos, implementa-se uma v2 da API.

Fase 5:
O software é escalado verticalmente, fazer uploads em multiplas máquinas (começamos a trabalhar com S3), muita refatoração para executar a escala vertical, criação de autoscaling, pipeline de CI/CD.

Fase 6:
Começamos a utilizar graphQL para algumas APIs, começam a aparecer bugs em função da mudança nos formatos, problemas com logs em mjiutias máquinas – começa-se a pensar em algum sistema para centralização, cria-se integração com plataforma de CRM.

Fase 7:
Começamos a ter inconsistência dos dados de CRM, a pensar em conteinerizar nossa aplicação – precisa-se repensar o CI/CD, memória, logs, nos livrar de código legado.

Fase 8:
Começamos a trabalhar com microsserviços, inicia-se compartilhando bancos, começamos a ter problemas de tracing, lentidão no sistema, custo fica elevado.

Fase 9:
Não conseguimos mais trabalhar com containers e vamos para Kubernetes, temos que refazer o CI/CD novamente, começa-se a ter problemas de resiliência, começamos a trabalhar com mensageria sem ter muita experiência e começamos a perder mensagens, começa-se a contratar consultoras de software para auxiliar.

Fase 10:
Use a imaginação!

A partir da análise dessas fases, podemos ver que um software não se perde do dia pra noite, mas sim um dia de cada vez. O sistema começa a ficar com cara de legado, com débitos técnicos que ninguém quer colocar a mão.
A seguir listo algumas reflexões desse ciclo de vida apresentado:

Faltou visão de futuro: não podemos ter a inocência de achar que um software sempre será simples/pequeno. Uma hora ele cresce;
Má definição de limites: não houve uma separação clara entre o que é negócio e o que é framework, gerando um acoplamento e dificultando a troca e adição de componentes;
Falta de preparação para escalamento: no início tudo está no mesmo servidor – cash, logs, upload. Porém, quando começamos escalar horizontalmente, e depois verticalmente, temos que refazer muita coisa. Logo, temos que pensar e nos preparar para escala desde o dia 1.
Falta de limites para otimizações frequentes: a todo momento o software vai precisar de novas features com prazos apertados e, se não definirmos um limite aceitável, começaremos a criar e manter cada vez mais débitos técnicos;
Incapaz de lidar com mudanças bruscas: ex.: empresa trocou a gateway de pagamento, ou de plataforma de CRM → conseguimos alterar facilmente a implementação?

Podemos ver que, se tivermos visão de futuro desde o dia 1, conseguimos criar camadas anti-corrupção para que as mudanças não afetem o negócio e não dificultem o nosso trabalho.

Arquitetura vs Design de Software

É importante também salientar a diferença entre esses dois conceitos, muitas vezes considerados como a mesma coisa. A seguir segue uma definição que considero bem sucinta e completa, resumida do seguinte artigo: https://eximia.co/quais-sao-as-diferencas-entre-arquitetura-e-design-de-software/

“Atividades relacionadas a arquitetura de software são sempre de design. Entretanto, nem todas as atividades de design são sobre arquitetura. O objetivo primário da arquitetura de software é garantir que os atributos de qualidades, restrições de alto nível e os objetivos do negócio, sejam atendidos pelo sistema. Qualquer decisão de design que não tenha relação com este objetivo não é arquitetural. Todas as decisões de design para um componente que não sejam “visíveis” fora dele, geralmente, também não são.”

Em outras palavras, podemos entender a arquitetura como aquilo faz entregarmos o software com qualidade, eficiência, e garantir que o negócio vai funcionar, algo mais abstrato. O Design seria como vamos fazer isso funcionar, algo mais concreto.

Muitas decisões de design não têm a ver com a arquitetura, porém também podem impactar nas decisões arquiteturais. Segue um exemplo:

“Todos os sistemas terão que gerar logs e ficarem armazenados em um local” – visão arquitetural

“Para que isso seja possível, vamos fazer o log não ser gravado em arquivo e sim sair pelo terminal” – visão de design

Arquitetura Hexagonal

De acordo com Alistair Cockburn, podemos definir a Arquitetura Hexagonal da seguinte forma:

“Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.”

De forma sucinta, é basicamente criar software isolando a forma que pessoas e sistemas o acessam, e como ele acessa outros sistemas. Podemos entender melhor esse conceito a partir da seguinte imagem:

Observando a imagem, temos o coração de nossa aplicação, o negócio, no centro do hexágono. Para haver a comunicação com objetos externos (tanto cliente quanto servidor abstraídos por seus respectivos adaptadores) criamos interfaces (ports), de forma a impedir que minha aplicação (negócio) fique diretamente interligada ao adaptador e vice-versa.

É importante notar aqui que a forma de hexágono é apenas uma estrutura arbitrária para representar – poderia ser qualquer outro formato. A questão é que não queremos misturar o que é complexidade de negócio com a complexidade técnica.

Nesta linha, conseguimos componentizar a integração com os diversos objetos externos: logs, cache, banco, filas de mensagens, cliente (REST, gRPC, GraphQL), facilitando sua adição e substituição por meio dos “Ports” e “Adapters”.

De forma mais prática, precisamos utilizar de um dos conceitos de SOLID – Princípio da Inversão de Dependência – para atingir essa clareza:

Módulos de alto nível (objetos externos) não dependem de módulos de baixo nível (negócio), e sim ambos devem depender de abstrações.
Abstrações não devem depender de detalhes e vice-versa.

O que fazemos aqui é uma inversão de controle. Por exemplo, se eu instanciar uma classe B dentro de outra classe A diretamente, estarei gerando um acoplamento. O que farei ao invés disso é recebê-la dentro do construtor. Porém, para evitar esse acoplamento entre as duas classes, devo receber uma interface no construtor de A, que representará B indiretamente.

Todos esses conceitos mudam completamente a forma como desenvolvemos software de qualidade.

Hexagonal vs Clean vs Onion

Por fim, podemos diferenciar esses conceitos muitas vezes confundidos. Na arquitetura hexagonal não há um padrão estabelecido de como o código deve ser organizado, quais camadas ou estruturas de pastas utilizar, diferentemente da Clean e Onion Architecture. Ela apenas define o princípio da separação entre o coração da aplicação e os objetos externos.

Neste meio, não precisamos necessariamente implementar uma estrutura conhecida (Clean/Onion) contanto que sigamos alguma regra de organização seguindo a essência dos conceitos que falamos neste artigo. Mas é claro, implementar estruturas já estudadas e conhecidas com certeza facilita bastante o processo.