Wenn gewöhnliches CRUD nicht mehr funktioniert: CQRS und Event Sourcing in C++ kennenlernen
Haben Sie jemals die Situation erlebt, dass Ihr C++-Programm, das mit Testdaten einwandfrei funktioniert, in der Produktion ins Stocken gerät? Oder wenn Business-Analysten den Änderungsverlauf sehen möchten, Sie aber nur den letzten Zustand haben? Kommt Ihnen bekannt vor? Dann willkommen in der Welt von CQRS (Command Query Responsibility Segregation) und Event Sourcing.
Diese beiden Patterns sind nicht nur modische Begriffe aus der Welt der Microservices. Sie sind leistungsstarke Werkzeuge, mit denen Sie skalierbare, auditierbare und flexible Systeme erstellen können. Und ja, sie passen hervorragend zu C++ – einer Sprache, in der Leistung und Speicherkontrolle entscheidend sind.
In diesem Artikel halten wir keine langweiligen Vorträge. Wir werden erläutern, wie man CQRS und Event Sourcing in reinem C++ ohne Framework-Magie implementiert, mit echten Beispielen und gebrauchsfertigen Vorlagen.
CQRS: Lesen und Schreiben trennen
In der klassischen CRUD-Architektur (Create, Read, Update, Delete) verwenden wir dasselbe Modell sowohl zum Lesen als auch zum Schreiben. Das ist praktisch, solange das System nicht wächst. Probleme treten auf, wenn:
- Das Objekt im Speicher zu komplex ist, um schnell für das Lesen serialisiert zu werden.
- Die Geschäftslogik des Schreibens eine Validierung erfordert, die das Lesen verlangsamt.
- Verschiedene Teile des Systems dieselben Daten auf unterschiedliche Weise sehen möchten.
CQRS bietet eine einfache, aber geniale Lösung: Teilen Sie das Modell in zwei Teile – Command (Befehl) zum Ändern des Zustands und Query (Abfrage) zum Lesen. Sie können unterschiedliche Speicher, unterschiedliche Datenstrukturen verwenden und sogar auf verschiedenen Maschinen implementiert sein.
Basisimplementierung von CQRS in C++
Beginnen wir einfach. Stellen Sie sich vor, wir schreiben ein Auftragsverwaltungssystem. Anstatt alles in einer Klasse Order zu speichern, teilen wir die Zuständigkeiten auf.
Zuerst definieren wir die Schnittstellen für Befehle und Abfragen:
#include <memory>#include <vector>#include <string>
// Basisklasse für alle Befehleclass Command {public: virtual ~Command() = default; virtual void execute() = 0;};
// Basisklasse für alle Abfragentemplate <typename TResult>class Query {public: virtual ~Query() = default; virtual TResult execute() = 0;};Jetzt erstellen wir einen konkreten Befehl zum Hinzufügen eines Artikels zu einer Bestellung:
class AddItemCommand : public Command {private: std::string orderId_; std::string productId_; int quantity_; // Verweis auf das Repository (oder den Command Bus) OrderRepository& repository_;
public: AddItemCommand(const std::string& orderId, const std::string& productId, int quantity, OrderRepository& repo) : orderId_(orderId), productId_(productId), quantity_(quantity), repository_(repo) {}
void execute() override { // Geschäftslogik: Prüfen, ob der Artikel auf Lager ist if (quantity_ <= 0) { throw std::invalid_argument("Quantity must be positive"); } // Domänenservice oder Aggregat aufrufen auto& order = repository_.getById(orderId_); order.addItem(productId_, quantity_); repository_.save(order); }};Und so sieht eine Abfrage zum Abrufen einer Bestellübersicht aus:
class OrderSummaryQuery : public Query<OrderSummary> {private: std::string orderId_; ReadModelDatabase& db_;
public: OrderSummaryQuery(const std::string& orderId, ReadModelDatabase& db) : orderId_(orderId), db_(db) {}
OrderSummary execute() override { // Aus der optimierten Lesedatenbank lesen return db_.getOrderSummary(ord