Arquitetura Hexagonal em C++: como parar de temer mudanças e amar os testes
\n\nVocê já reescreveu um módulo de banco de dados e depois percebeu que precisava alterar metade da lógica de negócio? Ou tentou cobrir com testes unitários uma classe que arrasta três toneladas de dependências do mundo externo? Se sim — bem-vindo ao clube. Felizmente, temos uma ferramenta que resolve esses problemas de uma vez por todas: a arquitetura hexagonal (também conhecida como arquitetura Ports & Adapters).
\n\nNeste artigo, vamos analisar como aplicar esse conceito poderoso em C++. Você verá não diagramas abstratos, mas código real que pode ser usado agora mesmo. E sim, faremos isso sem fanatismo — de modo que nem o projeto legado exploda e o novo código permaneça limpo.
\n\nO que é arquitetura hexagonal (e qual a relação com o hexágono)?
\n\nImagine o núcleo da sua aplicação — é a lógica de negócio pura, que não conhece bancos de dados, REST API ou interface gráfica. Ao redor desse núcleo estão os portos (interfaces), e mais além, os adaptadores (implementações concretas). Visualmente, isso lembra um hexágono, daí o nome.
\n\nA ideia central: as dependências apontam para dentro. O mundo externo depende do núcleo, e não o contrário. Isso permite trocar o banco de dados, o protocolo de comunicação ou a interface do usuário sem alterar a lógica de negócio. Parece um sonho? Vamos ver como implementar isso em C++.
\n\nO problema clássico: alto acoplamento
\n\nAssim é como geralmente se parece um código C++ típico:
\n\nclass OrderService {\npublic:\n void createOrder(const OrderData& data) {\n // Lógica de negócio\n if (data.items.empty()) throw std::runtime_error("Pedido vazio");\n\n // E aqui está o problema — dependência direta do banco de dados\n Database db("localhost", 5432);\n db.connect();\n db.saveOrder(data);\n db.disconnect();\n \n // E também do serviço de email\n EmailSender sender("smtp.example.com");\n sender.sendInvoice(data);\n }\n};\n\nO que há de errado aqui? Tudo. Você não pode testar createOrder sem um banco de dados real e um servidor de email. Se amanhã decidir migrar de PostgreSQL para MongoDB — terá que reescrever OrderService. E se o serviço de email se tornar assíncrono via fila — novamente alterações na lógica de negócio.
Resolvendo o problema com arquitetura hexagonal
\n\nPasso 1: Definindo os portos (interfaces)
\n\nUm porto é um contrato que define o que o núcleo precisa do mundo externo, mas não como é implementado.
\n\n// port/OrderRepository.h\nclass OrderRepository {\npublic:\n virtual ~OrderRepository() = default;\n virtual void save(const Order& order) = 0;\n virtual std::optional<Order> findById(const std::string& id) = 0;\n};\n\n// port/NotificationService.h\nclass NotificationService {\npublic:\n virtual ~NotificationService() = default;\n virtual void sendInvoice(const Order& order) = 0;\n};\n\nObserve: são classes abstratas puras. Sem dependências de frameworks, bibliotecas ou serviços externos. Apenas entidades de negócio.
\n\nPasso 2: Escrevendo o núcleo (lógica de negócio)
\n\nAgora nosso serviço depende apenas de interfaces, não de implementações concretas:
\n\n// core/OrderService.h\nclass OrderService {\npublic:\n OrderService(OrderRepository& repo, NotificationService& notifier)\n : repository_(repo), notifier_(notifier) {}\n\n void createOrder(const OrderData& data) {\n // Validação — lógica de negócio pura\n if (data.items.empty()) {\n throw std::invalid_argument("O pedido não pode estar vazio");\n }\n\n Order order = Order::create(data);\n repository_.save(order);\n notifier_.sendInvoice(order);\n }\n\nprivate:\n OrderRepository& repository_;\n NotificationServ