CQRS и Event Sourcing в C++: Архитектура для сложных систем

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Когда обычный CRUD перестаёт работать: знакомимся с CQRS и Event Sourcing на C++

Вы когда-нибудь сталкивались с ситуацией, когда ваша C++ программа, прекрасно работающая на тестовых данных, начинает «тормозить» в production? Или когда бизнес-аналитики просят показать историю изменений, а у вас — только последнее состояние? Знакомо? Тогда добро пожаловать в мир CQRS (Command Query Responsibility Segregation) и Event Sourcing.

Эти два паттерна — не просто модные слова из мира микросервисов. Это мощные инструменты, которые позволяют строить масштабируемые, аудитируемые и гибкие системы. И да, они отлично ложатся на C++ — язык, где производительность и контроль над памятью имеют решающее значение.

В этой статье мы не будем читать скучные лекции. Мы разберём, как реализовать CQRS и Event Sourcing на чистом C++ без магии фреймворков, с реальными примерами и готовыми к использованию шаблонами.

CQRS: Разделяем чтение и запись

В классической архитектуре CRUD (Create, Read, Update, Delete) мы используем одну и ту же модель и для чтения, и для записи. Это удобно, пока система не начинает расти. Проблемы возникают, когда:

  • Объект в памяти слишком сложный, чтобы быстро сериализоваться для чтения.
  • Бизнес-логика записи требует валидации, которая замедляет чтение.
  • Разные части системы хотят видеть одни и те же данные по-разному.

CQRS предлагает простое, но гениальное решение: разделить модель на две части — Command (команда) для изменения состояния и Query (запрос) для чтения. Они могут использовать разные хранилища, разные структуры данных и даже быть реализованы на разных машинах.

Базовая реализация CQRS на C++

Начнём с простого. Представьте, что мы пишем систему управления заказами. Вместо того чтобы хранить всё в одном классе Order, мы разделим обязанности.

Сначала определим интерфейсы для команд и запросов:

#include <memory>
#include <vector>
#include <string>

// Базовый класс для всех команд
class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
};

// Базовый класс для всех запросов
template <typename TResult>
class Query {
public:
    virtual ~Query() = default;
    virtual TResult execute() = 0;
};

Теперь создадим конкретную команду для добавления товара в заказ:

class AddItemCommand : public Command {
private:
    std::string orderId_;
    std::string productId_;
    int quantity_;
    // Ссылка на репозиторий (или шину команд)
    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 {
        // Бизнес-логика: проверяем, есть ли товар на складе
        if (quantity_ <= 0) {
            throw std::invalid_argument("Quantity must be positive");
        }
        // Вызываем доменный сервис или агрегат
        auto& order = repository_.getById(orderId_);
        order.addItem(productId_, quantity_);
        repository_.save(order);
    }
};

А вот так выглядит запрос для получения сводки по заказу:

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 {
        // Читаем из оптимизированной БД для чтения
        return db_.getOrderSummary(orderId_);
    }
};

Важно: Команды ничего не возвращают (void), они только меняют состояние. Запросы возвращают данные, но не меняют состояние. Это жёсткое правило — основа CQRS.

Event Sourcing: Храним историю, а не состояние

Теперь переходим ко второму паттерну — Event Sourcing. Если CQRS разделяет чтение и запись, то Event Sourcing меняет способ хранения данных.

Вместо того чтобы хранить текущее состояние объекта (например, «заказ №5: статус «оплачен»»), мы храним последовательность событий, которые к этому состоянию привели. Например:

  • Событие 1: «Заказ создан»
  • Событие 2: «Товар "Книга" добавлен»
  • Событие 3: «Оплата получена»

Текущее состояние — это просто результат применения всех этих событий по порядку. Это даёт нам суперсилу: мы можем «отмотать» время назад, увидеть состояние системы на любую дату, и, что самое важное, у нас есть полный аудит.

Реализация Event Sourcing на C++

Для начала нам нужна структура события и агрегат, который эти события применяет.

#include <memory>
#include <vector>
#include <variant>
#include <string>
#include <chrono>

// Базовое событие
struct DomainEvent {
    std::string aggregateId;
    std::chrono::system_clock::time_point timestamp;
};

// Конкретные события
struct OrderCreated : DomainEvent {
    std::string customerId;
};

struct ItemAdded : DomainEvent {
    std::string productId;
    int quantity;
};

struct PaymentReceived : DomainEvent {
    double amount;
};

// Агрегат — корень для нашего заказа
class OrderAggregate {
private:
    std::string id_;
    std::string customerId_;
    std::vector<std::string> items_;
    double totalAmount_ = 0.0;
    bool isPaid_ = false;

    // Применение событий для восстановления состояния
    void apply(const OrderCreated& event) {
        id_ = event.aggregateId;
        customerId_ = event.customerId;
    }

    void apply(const ItemAdded& event) {
        items_.push_back(event.productId);
        totalAmount_ += event.quantity * 10.0; // Упрощённо
    }

    void apply(const PaymentReceived& event) {
        isPaid_ = true;
        totalAmount_ -= event.amount;
    }

public:
    // Восстанавливаем агрегат из событий
    static OrderAggregate loadFromHistory(
        const std::vector<DomainEvent>& events) {
        OrderAggregate agg;
        for (const auto& event : events) {
            std::visit([&agg](const auto& e) { agg.apply(e); }, event);
        }
        return agg;
    }

    // Метод, который генерирует событие
    std::vector<DomainEvent> addItem(const std::string& productId, int qty) {
        std::vector<DomainEvent> newEvents;
        if (isPaid_) {
            throw std::runtime_error("Cannot modify paid order");
        }
        ItemAdded event;
        event.aggregateId = id_;
        event.timestamp = std::chrono::system_clock::now();
        event.productId = productId;
        event.quantity = qty;
        newEvents.push_back(event);
        return newEvents;
    }

    const std::string& getId() const { return id_; }
};

Обратите внимание: addItem

Блоги

Рекомендаций по книгам