A Arquitetura Hexagonal promove a modularidade, flexibilidade e manutenibilidade do código, permitindo que as mudanças nos componentes externos não afetem diretamente a lógica de negócios. Essa abordagem é especialmente útil em sistemas nos quais a evolução das regras de negócio é frequente, e a capacidade de adaptação a mudanças é crucial.
A estrutura proposta por Alister Cockburn em seu artigo, visa o isolamento da camada de aplicação (core do negócio) fornecendo portas para as implementações de entrada e saída da aplicação, para atender os padrões propostos pela arquitetura, usamos o seguinte modelo de estrutura para implementação
Através dessa estrutura podemos organizar nosso projeto da seguinte forma:
-
Núcleo de Domínio (Core Domain): O núcleo da aplicação, onde residem as regras de negócio e as entidades principais, é encapsulado em um espaço conhecido como o "núcleo de domínio." Este é o coração da aplicação e contém as lógicas críticas para o negócio.
-
Portas (Ports): No contexto da arquitetura hexagonal, uma porta é uma interface que define um conjunto de operações específicas no núcleo de domínio. As portas são implementadas pelos adaptadores externos.
-
Portas de Entrada (input port): Representam os pontos de entrada para o núcleo de domínio. Como nota existe uma interpretação Literal vs. Metáfora das portas de entrada no qual, literalmente falando, em uma arquitetura hexagonal, as portas em geral são as interfaces como Repository, NotificationService, etc., que são implementadas pelos adaptadores. No entanto, se considerarmos a metáfora de uma "porta" como um ponto de acesso ao sistema, então os casos de uso representam, metaforicamente, essas portas de entrada, pois eles iniciam a interação com o núcleo do sistema, ou seja, não há necessidade de uma implementação literal de uma interface de porta, pois o UseCase por si só já é a entrada ao núcleo do sistema.
-
Portas de Saída (output port): Representam os pontos de saída (banco de dados, chamadas a client externos, publisher de mensagerias etc), todas implementadas pelos adaptadores de output. As portas em geral são as interfaces como Repository, NotificationService, etc. Diferentemente das portas de entrada, sua interface é obrigatória, pois não há como realizar uma implementação sem sua existencia.
-
-
Adaptadores de Input (Input Adapters): Esses adaptadores são responsáveis por lidar com as interações que entram na aplicação, no core, como requisições do usuário, eventos externos, etc. Eles implementam as portas de entrada para realizar suas operações no core do sistema e traduzem as operações específicas do núcleo de domínio para os formatos ou protocolos adequados. Esses adaptadores encaminham as solicitações do usuário para o núcleo de domínio e utilizam de portas de saida para realizar operações externas, como a chamada de uma API, ou uma iteração com um repositório.
-
Adaptadores de Output: As interações com elementos externos, como interfaces de usuário, bancos de dados, serviços externos, etc., são tratadas através de adaptadores externos. Esses adaptadores implementam interfaces das portas de output que o núcleo de domínio pode usar, isolando-o das implementações específicas e facilitando a substituição e testes.
Uma excelente maneira de garantir a integridade da arquitetura podemos usar bibliotecas conhecidas como o ArchUnit ou o Konsist (para Kotlin), Segue um exemplo de implementação para a validação da arquitetura proposta por esse artigo
Dependência:
<dependency>
<groupId>com.lemonappdev</groupId>
<artifactId>konsist</artifactId>
<version>${konsist.version}</version>
<scope>test</scope>
</dependency>
O Teste:
package {your-package}.arch
import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.architecture.KoArchitectureCreator.assertArchitecture
import com.lemonappdev.konsist.api.architecture.Layer
import org.junit.jupiter.api.Test
class ArchitectureTest {
@Test
fun `hexagonal architecture layers have correct dependencies`() {
Konsist
.scopeFromProduction()
.assertArchitecture {
val core = Layer("Core", "{your-package}.core..")
val adapters = Layer("Adapters", "{your-package}.adapter..")
val config = Layer("Config", "{your-package}.config..")
config.dependsOn(adapters, core)
adapters.dependsOn(core)
core.dependsOnNothing()
}
}
@Test
fun `hexagonal architecture layers have correct dependencies details`() {
Konsist
.scopeFromProduction()
.assertArchitecture {
val corePortsInput = Layer("Core Ports Input", "{your-package}.core.ports.input..")
val corePortsOutput = Layer("Core Ports Output", "{your-package}.core.ports.output..")
val coreUseCases = Layer("Core Use Cases", "{your-package}.core.usecases..")
val coreEntities = Layer("Core Entities", "{your-package}.core.entities..")
val adaptersInput = Layer("Adapters Input", "{your-package}.adapter.input..")
val adaptersOutput = Layer("Adapters Output", "{your-package}.adapter.output..")
val config = Layer("Config", "{your-package}.config..")
config.dependsOn(
adaptersOutput,
adaptersInput,
corePortsOutput,
corePortsInput,
coreUseCases,
coreEntities
)
adaptersInput.dependsOn(corePortsInput, coreEntities)
adaptersOutput.dependsOn(corePortsOutput, coreEntities)
coreUseCases.dependsOn(corePortsInput, corePortsOutput, coreEntities)
corePortsInput.dependsOn(coreEntities)
corePortsOutput.dependsOn(coreEntities)
coreEntities.dependsOnNothing()
}
}
}
Referências
Veja algumas referências importantes sobre o assunto
-
Falando sobre o Core Domain: https://medium.com/@guilherme.zarelli/o-core-domain-modelando-dom%C3%ADnios-ricos-f1fe664c998f
-
Mais sobre ArchUnit e garantias de arquitetura por testes: https://medium.com/luizalabs/garantindo-a-arquitetura-de-uma-aplica%C3%A7%C3%A3o-sem-complexidade-6f675653799c
-
Exemplo com Java + ArchUnit + Clean Architecture: https://github.com/gbzarelli/jynx/blob/master/src/test/java/br/com/helpdev/jynx/archunit/CleanArchUnitTest.java
-
Descomplicando a Clean Architecture: https://medium.com/luizalabs/descomplicando-a-clean-architecture-cf4dfc4a1ac6