Hi, I'm João Vanzuita, a Backend software developer.

Arquitetura Hexagonal (usando Go Lang)

Notas: todo o material contido aqui é baseado no artigo original do autor da arquitetura hexagonal (Alistair Cockburn) https://alistair.cockburn.us/hexagonal-architecture/

Software é um pedaço de matéria intangível, que cresce naturalmente, assim como sua complexidade. Quanto maior a complexidade do software, mais difícil é para mante-lo.

Todos nós queremos escrever software de qualidade, e que seja fácil de manter. É esse o ponto de se utilizar um padrão já testado de arquitetura que se encaixa com o objetivo da sua aplicação. Software bem estruturado, onde você pode facilmente navegar e entender, é o que toda pessoa desenvolvedora de software que gosta o que faz, quer alcançar.

Este artigo fala sobre arquitetura hexagonal.

Intenção

O Alistair Cockburn define uma longa intenção, vamos quebrar isso em pedaços.

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.

Permitir que o software seja executado de forma independente e isolado.

When any driver wants to use the application at a port, it sends a request that is converted by an adapter for the specific technology of the driver into an usable procedure call or message, which passes that to the application port. The application is blissfully ignorant of the driver’s technology. When the application has something to send out, it sends it out through a port to an adapter, which creates the appropriate signals needed by the receiving technology (human or automated). The application has a semantically sound interaction with the adapters on all sides of it, without actually knowing the nature of the things on the other side of the adapters.

Os componentes que fazem parte da arquitetura trabalham de forma independente, quando algo precisa ser executado, esse algo segue um caminho natural, sem expor detalhes de implementação. Cada parte da arquitetura guarda um pedaço da solução, mas nenhuma delas realmente depende da outra.

nota: Traduzindo literalmente, o autor expões muito mais detalhes do que acontece na arquitetura, mas para esse momento, é importante não afundar o leitor de informação, mas sim dar um breve entendimento do que está por vir.

Motivação

An interesting similar problem exists on what is normally considered “the other side” of the application, where the application logic gets tied to an external database or other service. When the database server goes down or undergoes significant rework or replacement, the programmers can’t work because their work is tied to the presence of the database. This causes delay costs and often bad feelings between the people.

Há uma longa descrição no artigo original, sinta-se livre para ler o artigo original, mas vou me ater a ir direto ao ponto.

A sua aplicação sendo escrito para um banco de dados específico, quando surge a necessidade de se utilizar outro banco de dados, sua aplica estar amarrada a sua escolha de banco de dados inicial. No momento de transição para o novo banco de dados, toda a implementação de chamadas terá que ser reescrita, assim como a maioria dos testes unitários.

Com a abordagem de injeção de dependência descrita acima, Substituir uma tecnologia por outra se torna mais fácil, seja trocar um banco de dados por outro, ou uma ferramenta CLI por outra, ou serviço de autenticação por outro.

O que o Hexágono representa na arquitetura hexagonal ?

Many applications have only two ports: the user-side dialog and the database-side dialog. This gives them an asymmetric appearance, which makes it seem natural to build the application in a one-dimensional, three-, four-, or five-layer stacked architecture.

Arquitetura Hexagonal

É meramente uma opção visual do Alistair Clockburn. Alistair quer reforçar que um hexágono da espaço e a ideia de que várias "coisas/portas" podem se conectar ao software.

A escolha vem da percepção de que a representação tradicional de arquitetura por camadas, da a ideia de direção única de comunicação (de dentro p/ fora, ou de fora p/ dentro), e não é identificada como algo que pode haver várias coisas (ports) que se conectam ao software.

Ports and Adapters ou Arquitetura Hexagonal ?

Arquitetura hexagonal foi o primeiro nome definido, mais tarde atualizado para Ports and Adapters, mas o que pegou mesmo foi arquitetura hexagonal.

Porque utilizar arquitetura hexagonal ?

1. Código independente da lógica de negócio

Software que permite facilidade de troca de componentes, habilita o desenvolvedor a facilmente substituir tecnologias utilizadas no projeto. Se hoje usa-se MySQL, e amanhã é necessário substituí-lo para usar PostgreSQL, a pessoa desenvolvedora apenas substitui a implementação de banco de dados seguindo a interface previamente definida para a camada de persistência.

2. Código isolado facilita testes

Sem a dependência de componentes externos para o código rodar, criar mocks que retornam valores esperados se torna mais fácil.

3. Código direcionado a utilizar interfaces

A ideia de ports, trás a clareza de que adapters funcionam por um propósito bem definido. Mudanças de propósito, se refletem na mudança do ports. Quando há a necessidade de se substituir um adapter, há um contrato bem definido (ports) do que deve ser implementado.

4. Se encaixa bem com o conceito de Domain Driven Design

A arquitetura hexagonal também utiliza-se da ideia do DDD, o core é uma parte isolada da aplicação, que recebe requisições apenas dos adapters, deixando o domínio que toma as decisões de negócio isolado.

Implementado arquitetura hexagonal em Go Lang

Criei esse projeto exemplo que implementa a arquitetura em Go, e vou passa a passo comentar as partes mais importantes, explicando e exemplificando os diferentes componentes que fazem parte da arquitetura.

Antes de olhar o código, a árvore de arquivos da uma ideia geral de como foi feita a implementação:

1cmd
2│   ├── cli
3│   │   └── main.go
4│   └── httpserver
5│       └── main.go
6├── go.mod
7├── go.sum
8├── internal
9│   ├── domain
10│   │   ├── account.go
11│   │   ├── account_test.go
12│   │   ├── health.go
13│   │   ├── health_test.go
14│   │   └── payment.go
15│   ├── handlers
16│   │   ├── account.go
17│   │   ├── health.go
18│   │   └── payment.go
19│   ├── repositories
20│   │   ├── memory-db.go
21│   │   ├── memory-db_test.go
22│   │   └── sqlite-db.go
23│   └── services
24│       ├── account-service.go
25│       ├── account-service_test.go
26│       ├── health-service.go
27│       └── payment-service.go
28└── README.md

Entrando em mais detalhes, começando pelo drivers que estão na pasta cmd/cli. Há dois drivers, o CLI e o server HTTP.

Driver e Driven

O CLI é um driver na arquitetura hexagonal porque ele é um caminho de comunicação iniciada pelo ator e irá interagir com o domínio/business logic, contrário dos atores driven, que são atores que são iniciados pelo domínio/business logic.

O CLI executa algumas coisas simples, mas já da uma direção p/ onde isso está nos levando.

1func main() {
2   //dbRepository := repositories.NewMemoryDb()
3   dbRepository := repositories.NewSqliteDB()
4
5   acc := domain.Account{
6      Money: 100,
7   }
8
9   srv := services.NewAccountService(dbRepository)
10   id, err := srv.Create(acc)
11   if err != nil {
12      log.Println(err)
13   }
14   err = srv.WithdrawFromAccount(id, 50)
15   if err != nil {
16      log.Println(err)
17      return
18   }
19
20   balance, err := srv.Balance(id)
21   if err != nil {
22      log.Println(err)
23      return
24   }
25
26   fmt.Println(balance)
27}

Ports e Adapters

A linha 1 e 2 já nos mostram algo muito forte na arquitetura hexagonal, que é a facilidade de ter diferentes implementações p/ uma necessidade específica (injeção de dependência). Nesse caso há duas opções de banco de dados disponíveis, Memory DB e SQLite, e logo abaixo na linha 9 há um construtor chamado NewAccountServiceb, que espera "alguém" que satisfaça sua definição do que um repositório(db) pode fazer.

O que esse "alguém que satisfaça sua definição" quer dizer é que, há um Port que define como um repositório(db) deve se parecer. O Port por sua vez, é uma interface, que em Go é comumente definido como um contrato como outros podem implementa-lo seguindo sua definição.

Deixando essa sopa de letrinhas mais clara; O Port está definindo como um repositório deve se parecer, mas realmente não se importar como o repositório foi desenvolvido. E é por isso que:

1srv := services.NewAccountService(dbRepository)

Pode receber tanto o Memory DB como o SQLite. Se quiséssemos remover o SQLite, e utilizar o PostgreSQL, o definição do PostgreSQL tem que apenar seguir o contrato que o Port estabeleceu, e utiliza-lo no construtor do NewAccountServer no lugar do SQLite que está atualmente em uso.

Agora que Port ficou mais claro, você pode substituir o "alguém¨ citado acima, por Adapter. O adapter é esse alguém que segue o contrato definido pelo Port, no nosso caso Memory DB, SQLite e PostgreSQL.

Ok, mas como um Adapter nesse contexto pode ser criado?

Os Adapters, podem ser de dois tipos, Driver Adapters e Driven Adapters.

This is related to the idea from use cases of “primary actors” and “secondary actors”. A ‘’primary actor’’ is an actor that drives the application (takes it out of quiescent state to perform one of its advertised functions). A ‘’secondary actor’’ is one that the application drives, either to get answers from or to merely notify. The distinction between ‘’primary ‘’and’’ secondary ‘’lies in who triggers or is in charge of the conversation.

Em outras palavras, os Driver Adapters sao adaptadores que sao iniciadas pelo ator, (http server ou CLI nesse projeto), e Driven Adapters sao os adaptadores que sao iniciados pela aplicacao e vao em direcao ao ator.

Aqui ha um Driven Adapter, que implementa um repositorio, e sera iniciado pela aplicacao.

1package repositories
2
3import (
4   "database/sql"
5   "github.com/google/uuid"
6   _ "github.com/mattn/go-sqlite3"
7   "hexagonal-example/internal/domain"
8   "log"
9)
10
11type sqliteDB struct {
12   sqlite *sql.DB
13}
14
15func NewSqliteDB() *sqliteDB {
16   db, err := sql.Open() // inicia uma conexao de DB
17   return &sqliteDB{
18      sqlite: db,
19   }
20}
21
22// como esse DB pode devolver um objeto domain.Account?
23func (db sqliteDB) GetAccount(accountId uuid.UUID) (*domain.Account, error) {}
24
25// como esse DB pode salvar um domain.Account?
26func (db sqliteDB) SaveAccount(account *domain.Account) {}
27
28// como esse DB pode salvar um domain.Payment?
29func (db sqliteDB) SavePayment(payment domain.Payment) error {}

Handler

Os handlers, ainda fazem parte do Driver HTTP, e e o handler que ira iniciar a comunicacao com o servico, que pode sua vez, ira se comunicar com a logico de negocio.

Services

Os servicos sao os unicos elementos na nossa arquitetura que se comunicam com o dominio/business logic.

<work in progress/not final>

Projeto completo no GitHub: https://github.com/Jacquin-Home/hexagonal-arch-bank-example