Arquitetura hexagonal

Guilherme Giácomo Simoes
5 min readOct 13, 2022

--

Esse é meu primeiro texto no medium, e vou falar um pouco sobre arquitetura hexagonal (ou Ports And Adapters, como preferir). O motivo de estar escrevendo esse texto, é porque eu quero consolidar o que acabei de ler no próprio site do Alistair Cockburn criador da arquitetura hexagonal.

Primeiro precisamos entender qual o problema a ser resolvido. Então aqui vai uma breve explicação:

Um dos maiores problema de quando criamos softwares pra qualquer que seja a plataforma, é que acoplamos nossa regra de negócio com coisas exteriores. Por exemplo, acoplar sua regra de negócio com a GUI ou o seu banco de dados atual. Porque isso é tão ruim? Bom, temos quatro grandes motivos:
1) Tenho minha regra de negócio espalhada (GUI, Repository, …), isso dificulta e muito minha manutenção do software.

2) Acoplo meu software no que não deveria estar acoplado. Por exemplo, um arquivo que comunica com o banco MySQL, jamais poderá conter uma regra de negócio, pois se eu quiser mudar o meu SGBD, caso seja atraente, eu terei dificuldades.

3) Não consigo testar corretamente meu software, porque parte da lógica que deveria ser testada está no mesmo arquivo que contém as querys de banco de dados ou depende de componentes visuais como tamanho do campo ou cor do botāo.

4) Tenho dificuldades em migrar um sistema dirigido por humanos, como um sistema desktop, para um sistema dirigido inteiramente por computadores, como micro serviços que se comunicam por filas.

Então, qual a solução?

Alistair Cockburn sugeriu que criássemos portas e adaptadores para resolver o problema do acoplamento. Como isso funcionaria?

Imagine seu computador, ele tem muitas portas de conexões (a não ser que utilize um MacBook Air, nesse caso só lamento), por exemplo: HDMI, USB, USB-C … Então caso eu queira conectar um mouse USB, vou colocar na porta HDMI? Não, vou colocar na porta USB. Porque? Porque é onde encaixa, simples assim (salvo exceções). Então, isso seria a porta de entrada. Mas OK, eu posso ter conectado vários mouses diferentes, de marcas diferentes, como meu S.O. vai conseguir reconhecer os sinais que estão sendo mandados? Simples, nós criamos um adaptador, para que o sinal elétrico enviado pelo mouse possa ser convertido para um modelo de sinal que o S.O. entenda.

Então, nessa arquitetura teríamos essas “portas” e adaptadores para o mundo exterior (SGBD, GUI, Controllers..), a famosa imagem a seguir ilustra bem como isso deveria parecer:

Imagem 1

Então como está explícito na imagem, o mundo exterior consegue “encaixar” nessa porta, e o “sinal” que ele manda é adaptado para o domínio da sua aplicação.

A imagem coloca uma requisição HTTP, GUI.. conectando na mesma porta, porque apesar de serem coisas distintas, elas se “encaixam” na mesma entrada. Do outro lado, Temos o DB Access Service e o in-memory database. Dois “banco de dados” conectados na mesma porta, porque se encaixam naquela entrada, e tem seus “sinais” adaptados.

Ok, mas oque seriam NA PRÁTICA, essas portas e adaptadores? Ora … interface e classes adaptadoras. Mas como diria o saudoso e controverso Linus Torvalds: “Falar é besteira, me mostre o código”. Então aqui vai.

Vamos supor que temos uma classe useCase que cria Clientes. Entāo, ela NÃO DEVE depender diretamente da ClientRepositoryMySQL, mas sim da ClientRepositoryInterface, e vai esperar a sua injeção:

export class CreateClient {
private readonly clientRepository: ClientRepositoryInterface;
constructor(clientRepository: ClientRepositoryInterface) {
this.clientRepository = clientRepository;
}
async execute(client: Client) {
this.clientRepository.save(client);
}
}

A ClientRepositoryInterface é a “porta” para a conexão no domínio, logo tudo o que se “encaixar” na “porta” vai conseguir ser injetado nesse useCase. Aqui está a implementação da “porta”:

export interface ClientRepositoryInterface {
save(client: Client) : Promise<Client>;
getAll(): Promise<Client[]>;
}

Essa classe CreateClient é um UseCases e faz parte do domínio do nosso software. Então ela é a classe que fica dentro do Hexágono. Enquanto que a próxima classe, é a que ficaria fora do hexagono:

export class ClientRepositoryMySQL implements ClientRepositoryInterface {private readonly repository: Repository<ClientMySQL>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(ClientMySQL);
}
getAll(): Promise<Client[]> {
return this.repository.find();
}
save(client: Client): Promise<Client> {
this.repository.save(client);
return client;
}
}

Esta ai um exemplo bem simples de como isso ficaria na prática. Criamos um Repository de MySQL para comunicação com o banco de dados real.

E se quiséssemos criar uma classe que fosse um mock para eventuais testes unitários? Sem problemas, ela só precisa se “encaixar na porta”, ou seja, ela precisa implementar a interface ClientRepositoryInterface:

export class ClientRepositoryMemory implements ClientRepositoryInterface {

private clients : Client[] = [];
getAll(): Promise<Client[]> {
return Promise.resolve(this.clients);
}
save(client: Client): Promise<Client> {
this.clients.push(client);
return Promise.resolve(client);
}
}

Porém, está faltando algo. Se a minha tabela `client` no banco de dados tem os campos: id_client e name_client, e na minha entidade Client tenho os campos idClient e nameClient, então precisamos de um adaptador, para que o meu domínio consiga lidar com esses dados:

export abstract class ClientMySQLAdapter {   static execute(clientMySQL: ClientMySQL): Client {      const client = new Client(clientMySQL.id_client, clientMySQL.name_client);      return client; 
}
}

Logo, preciso na minha classe ClientRepositoryMySQL chamar esse adapter quando for retornar o Client:

getAll(): Promise<Client[]> {   const clientMySQL = this.repository.find();
const client : Client = ClientMySQLAdapter.execute(clientMySQL);
return client;
}

É claro que, esses códigos só servem de exemplo, e eu os digitei aqui no próprio Medium, não os testei, então, não ligue caso tenha um errinho, o importante é entender o modelo de arquitetura

Bom, então nosso exemplo de código prático adaptado a um desenho, ficou algo como isso:

Imagem 2

Perceba que o DB e o Memory acessam o mesmo lado do Hexágono? Então, isso significa que eles acessam a mesma “porta” ou a mesma Interface. Ambos implementam a interface ClientRepositoryInterface, então, logo, ambos conseguem “encaixar” nessa entrada/porta.

Acho que podemos resumir todo esse texto em uma única frase: Nunca dependa de classes concretas, sempre de abstrações.

Jamais dependa de ClientRepositoryMySQL, mas sim do ClientRepositoryInterface. Nada mais é do que o princípio da substituição de Liskov, Princípio da segregação de interfaces e princípio da inversão de dependências juntos em uma arquitetura.

Caso você não tenha conhecimentos do SOLID, primeiro que nem deveria estar lendo esse artigo, segundo, aqui vai um bom texto sobre isso: SOLID

Antes de finalizar esse artigo, queria explanar uma questão da arquitetura hexagonal que eu não considero muito importante:

O Aliston Cockburn por algum motivo, separou o hexágono em dois lados distintos: User-Side API e também Data-Side APIs. Podemos ver isso na imagem 1, no inicio do artigo. O lado User-Side API, está obvio que é o lado onde cuida da comunicação com o client, como: GUI, Requisições HTTP, e assim por diante. Enquanto que no lado Data-Side API ficam as comunicações com banco de dados, InMemory, entre outros… Não vejo isso tendo um impacto profundo na arquitetura final do projeto, mas como isso é uma democracia você pode deixar sua contestação ao final desse texto.

Pra finalizar eu tenho um projeto onde eu aplico os conceitos falados nesse texto, mais conceitos de arquitetura limpa neste link

--

--