Quand le CRUD classique ne suffit plus : découvrez CQRS et Event Sourcing en C++
Avez-vous déjà rencontré une situation où votre programme C++, parfaitement fonctionnel avec des données de test, commence à « ralentir » en production ? Ou lorsque les analystes métier vous demandent de montrer l'historique des modifications, alors que vous ne disposez que du dernier état ? Cela vous parle ? Alors bienvenue dans le monde du CQRS (Command Query Responsibility Segregation) et de l'Event Sourcing.
Ces deux patrons ne sont pas de simples mots à la mode issus du monde des microservices. Ce sont des outils puissants qui permettent de construire des systèmes évolutifs, audités et flexibles. Et oui, ils s'intègrent parfaitement au C++ — un langage où la performance et le contrôle de la mémoire sont essentiels.
Dans cet article, nous n'allons pas faire de cours ennuyeux. Nous allons voir comment implémenter CQRS et Event Sourcing en C++ pur, sans la magie des frameworks, avec des exemples concrets et des modèles prêts à l'emploi.
CQRS : Séparer la lecture et l'écriture
Dans l'architecture CRUD classique (Create, Read, Update, Delete), nous utilisons le même modèle pour la lecture et l'écriture. C'est pratique tant que le système ne commence pas à croître. Les problèmes surviennent lorsque :
- L'objet en mémoire est trop complexe pour être sérialisé rapidement en lecture.
- La logique métier d'écriture nécessite une validation qui ralentit la lecture.
- Différentes parties du système souhaitent voir les mêmes données de manière différente.
CQRS propose une solution simple mais ingénieuse : diviser le modèle en deux parties — la Command (commande) pour modifier l'état et la Query (requête) pour la lecture. Elles peuvent utiliser des stockages différents, des structures de données différentes et même être implémentées sur des machines différentes.
Implémentation de base de CQRS en C++
Commençons par quelque chose de simple. Imaginez que nous écrivons un système de gestion de commandes. Au lieu de tout stocker dans une seule classe Order, nous séparons les responsabilités.
Définissons d'abord les interfaces pour les commandes et les requêtes :
#include <memory>#include <vector>#include <string>
// Classe de base pour toutes les commandesclass Command {public: virtual ~Command() = default; virtual void execute() = 0;};
// Classe de base pour toutes les requêtestemplate <typename TResult>class Query {public: virtual ~Query() = default; virtual TResult execute() = 0;};Créons maintenant une commande spécifique pour ajouter un article à une commande :
class AddItemCommand : public Command {private: std::string orderId_; std::string productId_; int quantity_; // Référence au repository (ou au bus de commandes) 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 { // Logique métier : vérifier si le produit est en stock if (quantity_ <= 0) { throw std::invalid_argument("Quantity must be positive"); } // Appeler le service domaine ou l'agrégat auto& order = repository_.getById(orderId_); order.addItem(productId_, quantity_); repository_.save(order); }};Et voici à quoi ressemble une requête pour obtenir un récapitulatif de commande :
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 { // Lire depuis la base de données optimisée pour la lecture return db_.getOrderSummary(ord