Arquitetura hexagonal

Toda vez que alguém fala em arquitetura hexagonal aparece um diagrama com quinze pastas, domain, application, infrastructure, e a conversa vira sobre onde colocar arquivo. O padrão não é isso.

Passei um tempo estudando o assunto para montar uma palestra, e os dois exemplos de código que aparecem aqui saíram direto dela.

💡 O padrão é do Alistair Cockburn, e as frases dele ao longo do texto estão traduzidas livremente por mim, a partir do artigo original e do livro.

Nivelando: o padrão adaptador Link para o cabeçalho

Metade do nome do padrão é adaptador. Ele é um dos clássicos do Design Patterns, a turma do GoF: converter a interface de uma classe na interface que outra espera. É o adaptador de tomada: o aparelho não muda, a parede não muda, a pecinha no meio faz os dois conversarem.

Guarde essa imagem, porque o hexágono é ela repetida em volta da aplicação inteira.

Só existem dois lados Link para o cabeçalho

Portas e Adaptadores tem apenas duas camadas: o lado de dentro (a aplicação) e o lado de fora (todo o resto).

O padrão traça uma linha só, entre o dentro e o fora. Camada de aplicação, de domínio, de infra: dividir o dentro assim é escolha sua, não exigência do hexágono.

Toda conversa atravessa uma porta, e quem traduz tecnologia para porta é um adaptador. O Cockburn avisa que porta não é classe: é o ponto de interação que a aplicação define para os atores externos. E ela nasce do propósito:

A mudança de design que fizeram foi projetar as interfaces do sistema por propósito, e não por tecnologia, e deixar as tecnologias substituíveis (em todos os lados) por adaptadores.

Ele recomenda que as funções de uma porta tenham nomes neutros de tecnologia, batizados pelo propósito de negócio da interação: tax_rate e não select_rate_from_postgres.

O teste é direto:

se o banco de dados deixar de ser um banco SQL e passar a ser um arquivo texto ou qualquer outro tipo de banco, a conversa através da API não deve mudar.

O exemplo Link para o cabeçalho

class TaxCalculator:
    def __init__(self, tax_rate_repository):
        self.tax_rate_repository = tax_rate_repository

    def tax_on(self, amount):
        return amount * self.tax_rate_repository.tax_rate(amount)


class FixedTaxRateRepository:
    def tax_rate(self, amount):
        return 0.15


tax_rate_repository = FixedTaxRateRepository()
my_calculator = TaxCalculator(tax_rate_repository)
print(my_calculator.tax_on(100))  # Outputs: 15.0

TaxCalculator não sabe de onde vem a alíquota. Quem decide é quem monta a aplicação. Trocar o FixedTaxRateRepository por um que lê do Postgres não encosta no cálculo.

Este padrão é simples assim: basta colocar uma API em volta de toda a sua aplicação ou sistema e passar um argumento para configurar cada conexão secundária. Nada além disso.

Não é sobre pastas Link para o cabeçalho

Sobre como organizar o lado de dentro, o livro tem uma seção inteira, e ela é basicamente um encolher de ombros:

Isso é totalmente com você. “Não é problema meu”, como se diz. Você pode fazer um design funcional ou orientado a objetos. Pode fazer um monólito modular ou uma grande bola de lama. Pode usar “clean architecture”, domain-driven design, ou qualquer coisa que te sirva. O padrão não diz absolutamente nada sobre esse assunto.

O mesmo hexágono, sem classe nenhuma, em um arquivo só:

def tax_on(tax_rate):
    def calculate_tax(amount):
        return amount * tax_rate(amount)

    return calculate_tax


def fixed_tax_rate(amount):
    return 0.15


tax_on(fixed_tax_rate)(100)  # Outputs: 15.0

Um closure faz o mesmo trabalho: tax_rate é a porta, fixed_tax_rate é o adaptador.

Lembre-se: em Portas e Adaptadores você é livre para organizar o lado de dentro da aplicação como quiser, e as coisas fora da aplicação como quiser.

E a interface? Link para o cabeçalho

Nos dois exemplos acima ninguém declarou interface nenhuma. Isso incomoda quem vem de Java, e o livro trata do assunto:

A estrutura de pastas não é coberta pelo padrão, nem é a mesma em todas as linguagens. Algumas linguagens (Java) exigem definições de interface. Outras (Python, Ruby) não. E algumas, como Smalltalk, nem têm o conceito de arquivos!

Em Python é assim mesmo: basta o objeto ter o método. Se quiser o contrato explícito para o type checker, use Protocol:

from typing import Protocol


class TaxRateRepository(Protocol):
    def tax_rate(self, amount: float) -> float: ...

Quando usar Link para o cabeçalho

O padrão cobra indireção e código de montagem, então só se paga onde substituir e isolar importam de verdade.

Vale considerar no subdomínio core, aquele que dá vantagem competitiva ao negócio. Seguindo a dica de Vlad Khononov, no Learning Domain-Driven Design, é ali que valem os padrões de modelagem mais sofisticados. Num CRUD de cadastro, provavelmente não.

Para se aprofundar Link para o cabeçalho

O artigo original do Alistair Cockburn é a fonte, e o livro Hexagonal Architecture Explained, dele com Juan Manuel Garrido de Paz, é a leitura definitiva. Recomendo muito. Para ver rodando, montei o pysmallerwebhexagon, versão em Python do exemplo do próprio Cockburn.

Então é isso, pessoal!

Até a próxima!

{}’s