Hexagonale Architektur in C++: Wie man aufhört, Änderungen zu fürchten, und Tests lieben lernt
\n\nHaben Sie jemals ein Datenbankmodul umgeschrieben und dann festgestellt, dass Sie die Hälfte der Geschäftslogik ändern müssen? Oder versucht, eine Klasse mit Unit-Tests zu testen, die drei Tonnen Abhängigkeiten aus der Außenwelt mit sich zieht? Wenn ja – willkommen im Club. Glücklicherweise gibt es ein Werkzeug, das diese Probleme ein für alle Mal löst: die hexagonale Architektur (auch bekannt als Ports & Adapters-Architektur).
\n\nIn diesem Artikel werden wir untersuchen, wie man dieses mächtige Konzept in C++ anwendet. Sie werden keine abstrakten Diagramme sehen, sondern echten Code, den Sie sofort verwenden können. Und ja, wir machen das ohne Fanatismus – so, dass weder ein Legacy-Projekt explodiert noch der neue Code unsauber wird.
\n\nWas ist hexagonale Architektur (und was hat das Sechseck damit zu tun)?
\n\nStellen Sie sich den Kern Ihrer Anwendung vor – das ist die reine Geschäftslogik, die weder von Datenbanken noch von REST-APIs oder grafischen Oberflächen weiß. Um diesen Kern herum befinden sich Ports (Schnittstellen) und noch weiter außen Adapter (konkrete Implementierungen). Visuell erinnert das an ein Sechseck (Hexagon), daher der Name.
\n\nDie Kernidee: Abhängigkeiten zeigen nach innen. Die Außenwelt hängt vom Kern ab, nicht umgekehrt. Dies ermöglicht es, die Datenbank, das Kommunikationsprotokoll oder die UI zu ändern, ohne die Geschäftslogik zu verändern. Klingt wie ein Traum? Schauen wir uns an, wie das in C++ umgesetzt wird.
\n\nDas klassische Problem: Enge Kopplung
\n\nSo sieht typischer C++-Code oft aus:
\n\nclass OrderService {\npublic:\n void createOrder(const OrderData& data) {\n // Geschäftslogik\n if (data.items.empty()) throw std::runtime_error("Bestellung ist leer");\n\n // Und hier ist das Problem – direkte Abhängigkeit von der Datenbank\n Database db("localhost", 5432);\n db.connect();\n db.saveOrder(data);\n db.disconnect();\n \n // Und auch vom E-Mail-Dienst\n EmailSender sender("smtp.example.com");\n sender.sendInvoice(data);\n }\n};\n\nWas ist hier falsch? Alles. Sie können createOrder nicht ohne eine echte Datenbank und einen E-Mail-Server testen. Wenn Sie morgen von PostgreSQL auf MongoDB umsteigen möchten, müssen Sie OrderService umschreiben. Und wenn der E-Mail-Dienst asynchron über eine Warteschlange wird – wieder Änderungen an der Geschäftslogik.
Das Problem mit hexagonaler Architektur lösen
\n\nSchritt 1: Ports (Schnittstellen) definieren
\n\nEin Port ist ein Vertrag, der definiert, was der Kern von der Außenwelt benötigt, aber nicht, wie es implementiert ist.
\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\nBeachten Sie: Dies sind reine abstrakte Klassen. Keine Abhängigkeiten von Frameworks, Bibliotheken oder externen Diensten. Nur Geschäftsobjekte.
\n\nSchritt 2: Den Kern (Geschäftslogik) schreiben
\n\nJetzt hängt unser Dienst nur von Schnittstellen ab, nicht von konkreten Implementierungen:
\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 // Validierung – reine Geschäftslogik\n if (data.items.empty()) {\n throw std::invalid_argument("Bestellung darf nicht leer sein");\n }\n\n Order order = Order::create(data);\n repository_.save(order);\n notifier_.sendInvoice(order);\n }\n\nprivate:\n OrderRepository& repository_;\n NotificationServ