Skip to content

Instantly share code, notes, and snippets.

@gbzarelli
Last active April 19, 2024 13:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gbzarelli/060b2f94f6620121232ad95b891f0e8d to your computer and use it in GitHub Desktop.
Save gbzarelli/060b2f94f6620121232ad95b891f0e8d to your computer and use it in GitHub Desktop.
Exemplo de implementação da Arquitetura Hexagonal com garantias por testes unitários

Arquitetura Hexagonal

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.

Estrutura

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

image

Através dessa estrutura podemos organizar nosso projeto da seguinte forma:

image

  • 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.

Garantindo a arquitetura por 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

Exemplo com Kotlin + Konsist

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment