Arquitectura Hexagonal en C++: Cómo Dejar de Temer los Cambios y Amar las Pruebas
\n\n¿Alguna vez ha reescrito un módulo de base de datos y luego se ha dado cuenta de que necesita cambiar la mitad de la lógica de negocio? ¿O ha intentado cubrir con pruebas unitarias una clase que arrastra tres toneladas de dependencias del mundo exterior? Si es así, bienvenido al club. Afortunadamente, tenemos una herramienta que resuelve estos problemas de una vez por todas: la arquitectura hexagonal (también conocida como arquitectura de Puertos y Adaptadores).
\n\nEn este artículo, analizaremos cómo aplicar este poderoso concepto en C++. Verá no esquemas abstractos, sino código real que puede usar ahora mismo. Y sí, lo haremos sin fanatismo, de manera que ni un proyecto heredado explote y el código nuevo sea limpio.
\n\n¿Qué es la Arquitectura Hexagonal (y qué tiene que ver el hexágono)?
\n\nImagine el núcleo de su aplicación: es la lógica de negocio pura que no sabe nada sobre bases de datos, API REST o interfaz gráfica. Alrededor de este núcleo se encuentran los puertos (interfaces), y más allá, los adaptadores (implementaciones concretas). Visualmente, esto se asemeja a un hexágono, de ahí el nombre.
\n\nLa idea clave: las dependencias apuntan hacia adentro. El mundo exterior depende del núcleo, no al revés. Esto permite cambiar la base de datos, el protocolo de comunicación o la interfaz de usuario sin modificar la lógica de negocio. ¿Suena como un sueño? Veamos cómo implementarlo en C++.
\n\nEl Problema Clásico: Alto Acoplamiento
\n\nAsí es como se ve a menudo el código C++ típico:
\n\nclass OrderService {\npublic:\n void createOrder(const OrderData& data) {\n // Lógica de negocio\n if (data.items.empty()) throw std::runtime_error("El pedido está vacío");\n\n // Y aquí está el problema: dependencia directa de la base de datos\n Database db("localhost", 5432);\n db.connect();\n db.saveOrder(data);\n db.disconnect();\n \n // Y también del servicio de correo electrónico\n EmailSender sender("smtp.example.com");\n sender.sendInvoice(data);\n }\n};\n\n¿Qué está mal aquí? Todo. No puede probar createOrder sin una base de datos real y un servidor de correo. Si mañana decide migrar de PostgreSQL a MongoDB, tendrá que reescribir OrderService. Y si el servicio de correo electrónico se vuelve asíncrono a través de una cola, de nuevo cambios en la lógica de negocio.
Resolviendo el Problema con la Arquitectura Hexagonal
\n\nPaso 1: Definir los Puertos (Interfaces)
\n\nUn puerto es un contrato que define qué necesita el núcleo del mundo exterior, pero no cómo se implementa.
\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: son clases abstractas puras. Sin dependencias de frameworks, bibliotecas o servicios externos. Solo entidades de negocio.
\n\nPaso 2: Escribir el Núcleo (Lógica de Negocio)
\n\nAhora nuestro servicio depende solo de interfaces, no de implementaciones 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 // Validación — lógica de negocio pura\n if (data.items.empty()) {\n throw std::invalid_argument("El pedido no puede estar vacío");\n }\n\n Order order = Order::create(data);\n repository_.save(order);\n notifier_.sendInvoice(order);\n }\n\nprivate:\n OrderRepository& repository_;\n NotificationServ