Arquitetura Hexagonal em Projetos Go (Golang)
Arquitetura Hexagonal em Projetos Go (Golang)
A arquitetura hexagonal, também conhecida como arquitetura de portas e adaptadores, é um padrão de design que promove a modularidade e a testabilidade em sistemas de software. Essa abordagem busca isolar o núcleo da lógica de negócios do restante do sistema, facilitando a manutenção, a evolução e a testagem automatizada. Ao aplicar a arquitetura hexagonal a projetos em Go (Golang), podemos aproveitar os benefícios dessa linguagem eficiente.
Entendendo a Arquitetura Hexagonal
A arquitetura hexagonal tem como princípio central a separação de preocupações (SoC - Separation of Concerns). Ela organiza o código em camadas, onde o núcleo da aplicação (lógica de negócios) fica isolado e independente de detalhes de implementação externos, como interfaces de usuário, bancos de dados e frameworks.
Principais Componentes:
- Domínio (Core): Contém a lógica de negócios da aplicação. Essa camada é agnóstica quanto à implementação externa e não contém dependências de frameworks ou bibliotecas específicas.
- Portas (Ports): Definem interfaces para interações externas. Isso inclui interfaces para serviços, repositórios, notificações, etc. As implementações concretas dessas interfaces são fornecidas por adaptadores.
- Adaptadores (Adapters): Implementam as interfaces definidas nas portas. Eles traduzem as chamadas do núcleo da aplicação para o mundo externo, lidando com detalhes de infraestrutura, como acesso a bancos de dados e interações com APIs externas.
Aplicando Hexagonal em Projetos Golang
Adaptando alguns conceitos e para criar a aplicação é possível chegar em um resultado simples e flexível altamente escalável. Vamos usar como exemplo uma aplicação que tem dois usos fundamentais, uma api REST, usada pela aplicação Front-end para comunicação e tráfego dos dados, uma api gRPC para comunicação entre serviços que define modelos de comunicação de uma forma extremamente rápida, leve e independente de linguagem.
REST vs gRPC
REST
- Utiliza Texto / JSON para se comunicar
- A comunicação é Unidirecional
- Alta latência
- Sem contrato ( maior chance de erros )
- Sem suporte a streaming
- Design pré-definido
- Bibliotecas de terceiros
gRPC
- Utiliza Protocol Buffers
- Comunicação Bidirecional e Assíncrona
- Baixa latência
- Contrato de comunicação bem definido (.proto)
- Suporte a Streaming
- Design é livre
- Geração de código
Estrutura de Diretórios:
Ao estruturar um projeto em Go com a arquitetura hexagonal, é comum organizar os diretórios da seguinte maneira:
├───application
│ ├───dtos
│ ├───grpc
│ └───api
│
├───cmd
| ├─── rest
| ├─── grpc
| └─── main
├───domain
| |───usecase
| |───services
│ └───model
└───adapters
├───db
└───repository
- cmd: Contém o ponto de entrada das aplicações.
- Application: Responsável por iniciar a aplicação e conectar os diferentes componentes, como os servidores ou Microsserviços.
- domain: Contém a lógica de negócios da aplicação.
- ports: Define as interfaces (portas) para interações externas.
- adaprters: Define os adaptadores, módulos de infra-estrutura como bancos de dados e repositórios.
Exemplo de Implementação:
Domínio (domain/service):
package domain type ProductService interface {
GetProductByID(id int) (*Product, error)
}
type Product struct {
ID int Name string Price float64
}
repository:
package ports
import "your_project/internal/domain"
type ProductRepository interface {
GetByID(id int) (*domain.Product, error)
}
Adaptador (Banco de dados):
package database
import (
"your_project/internal/domain"
"your_project/internal/ports"
)
type ProductRepository struct {
// implementa a interface ProductRepository
}
func (r *ProductRepository) GetByID(id int) (*domain.Product, error) {
// lógica de acesso ao banco de dados para obter o produto pelo ID
}
Porta (servidor gRPC):
func StartGrpcServer(database *DB, port int) {
grpcServer := grpc.NewServer()
reflection.Register(grpcServer)
pixRepository := repository.PixKeyRepositoryDB{Db: database}
pixUsecase := usecase.PixUseCase{PixKeyRepository: pixRepository}
pixGrpcService := NewgRPCService(pixUsecase)
pb.RegisterPixServiceServer(grpcServer, pixGrpcService)
addres := fmt.Sprintf("0.0.0.0:%d", port)
listener, err := net.Listen("tcp", addres)
if err != nil {
log.Fatalf("Not start gRPC server:%v", err)
}
log.Printf("grpc server start on port:%d", port)
errors := grpcServer.Serve(listener)
if errors != nil {
log.Fatalf("Not start gRPC server:%v", err)
}
}
CMD ponto de entrada do servidor gRPC:
func main() {
database := db.ConectDB(os.Getenv("env"))
grpc.StartGrpcServer(database, grpcPortNumber)
}
CMD ponto de entrada do servidor REST:
func main() {
database := db.ConectDB(os.Getenv("env"))
rest.StartRESTServer(database, restPortNumber)
}
Benefícios de Usar Hexagonal em Projetos Go
- Testabilidade: A separação de preocupações facilita a criação de testes unitários e de integração, permitindo testar a lógica de negócios independentemente das implementações de infraestrutura.
- Flexibilidade: A troca de implementações de infraestrutura (banco de dados, API, etc.) é facilitada, pois as mudanças são isoladas nos adaptadores.
- Manutenibilidade: A clareza na estrutura do código facilita a manutenção e evolução do software ao longo do tempo.
- Desacoplamento: O núcleo da aplicação não depende de detalhes de implementação, tornando-o mais independente e fácil de entender.
Ao aplicar a arquitetura hexagonal em projetos Go, os desenvolvedores podem criar sistemas mais modulares, testáveis e flexíveis, tirando proveito das características únicas da linguagem para construir software robusto e escalável.
Um ótimo exemplo de escalabilidade seria utilizarmos a aplicação para integrar um microserviço utilizando serviços de fila de mensagens como RabbitMQ, Kafka entre outros. Para integrar o serviço não seria necessário modificar o núcleo da aplicação, e sim somente criar um novo ponto de entrada, assim como o gRPC e o REST, e um novo comando para a aplicação, e o contrário também é verdadeiro, é possível modificar toda a regra de negócios da aplicação sem modificar as APIs. Isso mostra como é simples criar aplicações altamente escaláveis e simples adaptando alguns conceitos e trazendo para o contexto da aplicação.