Разработка программного обеспечения 3 мин чтения

Управление бизнес-логикой с потоками событий: Гибкие и отслеживаемые приложения с Event Sourcing

PROFSCODE Ekibi 08 июл 2026
Paylaş:
Паттерн Event Sourcing: Создание устойчивых и аудируемых систем

Введение: Почему стоит записывать события, а не только состояние?

Традиционные подходы к базам данных обычно хранят текущее состояние системы. Когда пользователь выполняет действие, данные в соответствующей таблице обновляются, и старое состояние теряется. Однако во многих сценариях знание того, почему и как что-то изменилось, более ценно, чем просто текущее состояние. Именно здесь вступает в игру паттерн Event Sourcing. Этот паттерн записывает каждое изменение состояния как событие и реконструирует текущее состояние системы как накопление этих событий.

Основные принципы Event Sourcing

Суть Event Sourcing заключается в том, что каждая бизнес-транзакция записывается как 'событие'. Эти события являются неизменяемыми записями, представляющими изменение в системе, и добавляются в хранилище событий (Event Store). Текущее состояние выводится путем последовательного применения этих событий.

  • Неизменяемые события: После создания событие никогда не может быть изменено или удалено.
  • Хранилище событий: База данных, где все события хранятся в хронологическом порядке.
  • Реконструкция состояния: Текущее состояние объекта всегда может быть восстановлено путем последовательного воспроизведения всех соответствующих событий.

Преимущества и варианты использования

  • Полная аудируемость: Предоставляет полную историю каждого изменения в системе.
  • Временные запросы: Легко запрашивать состояние системы в любой момент времени (например, баланс банка три дня назад).
  • Отладка и откат: Потоки событий могут быть исследованы для выявления проблем или отката нежелательных изменений.
  • Интеграция с CQRS: Естественным образом согласуется с паттерном разделения ответственности команд и запросов (CQRS), разделяя модели чтения и записи.
  • Богатый бизнес-анализ: Позволяет лучше понять причины и следствия изменений в бизнес-процессах.

Практический пример: Простой банковский счет

Давайте смоделируем приложение для банковского счета с использованием Event Sourcing. Создание счета, пополнение и снятие средств будут записываться как события.

1. Определения событий

Каждое событие будет классом, содержащим определенное действие и соответствующие данные.

public interface IEvent {}public class AccountCreatedEvent : IEvent{    public Guid AccountId { get; }    public string Owner { get; }    public DateTime CreatedAt { get; }    public AccountCreatedEvent(Guid accountId, string owner, DateTime createdAt)    {        AccountId = accountId;        Owner = owner;        CreatedAt = createdAt;    }}public class MoneyDepositedEvent : IEvent{    public Guid AccountId { get; }    public decimal Amount { get; }    public DateTime DepositedAt { get; }    public MoneyDepositedEvent(Guid accountId, decimal amount, DateTime depositedAt)    {        AccountId = accountId;        Amount = amount;        DepositedAt = depositedAt;    }}public class MoneyWithdrawnEvent : IEvent{    public Guid AccountId { get; }    public decimal Amount { get; }    public DateTime WithdrawnAt { get; }    public MoneyWithdrawnEvent(Guid accountId, decimal amount, DateTime withdrawnAt)    {        AccountId = accountId;        Amount = amount;        WithdrawnAt = withdrawnAt;    }}

2. Агрегат и применение событий

Агрегат BankAccount получает команды, генерирует события, а затем применяет эти события к своему собственному состоянию.

public class BankAccount{    public Guid Id { get; private set; }    public string Owner { get; private set; }    public decimal Balance { get; private set; }    private List<IEvent> _changes = new List<IEvent>();    public BankAccount()    {        Balance = 0;    }    public static BankAccount CreateNew(Guid accountId, string owner)    {        var account = new BankAccount();        account.ApplyChange(new AccountCreatedEvent(accountId, owner, DateTime.UtcNow));        return account;    }    public void Deposit(decimal amount)    {        if (amount <= 0) throw new ArgumentException("Сумма депозита должна быть больше нуля.");        ApplyChange(new MoneyDepositedEvent(Id, amount, DateTime.UtcNow));    }    public void Withdraw(decimal amount)    {        if (amount <= 0) throw new ArgumentException("Сумма снятия должна быть больше нуля.");        if (Balance < amount) throw new InvalidOperationException("Недостаточный баланс.");        ApplyChange(new MoneyWithdrawnEvent(Id, amount, DateTime.UtcNow));    }    public IEnumerable<IEvent> GetUncommittedChanges() => _changes;    public void MarkChangesAsCommitted() => _changes.Clear();    public void LoadFromHistory(IEnumerable<IEvent> history)    {        foreach (var e in history)        {            ApplyChange(e, isNew: false);        }    }    private void ApplyChange(IEvent e, bool isNew = true)    {        When((dynamic)e);        if (isNew)        {            _changes.Add(e);        }    }    private void When(AccountCreatedEvent e)    {        Id = e.AccountId;        Owner = e.Owner;        Balance = 0;    }    private void When(MoneyDepositedEvent e)    {        Balance += e.Amount;    }    private void When(MoneyWithdrawnEvent e)    {        Balance -= e.Amount;    }}

3. Хранилище событий (Event Store)

Хранилище событий сможет сохранять события и воспроизводить события для агрегата.

public interface IEventStore{    void SaveEvents(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion);    List<IEvent> GetEventsForAggregate(Guid aggregateId);}// Простая реализация In-Memory Event Storepublic class InMemoryEventStore : IEventStore{    private readonly Dictionary<Guid, List<IEvent>> _storage = new Dictionary<Guid, List<IEvent>>();    public void SaveEvents(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion)    {        if (!_storage.ContainsKey(aggregateId))        {            _storage.Add(aggregateId, new List<IEvent>());        }        var existingEvents = _storage[aggregateId];        // Оптимистическая проверка параллелизма        if (expectedVersion != -1 && existingEvents.Count != expectedVersion)        {            throw new InvalidOperationException("Конфликт параллелизма. Ожидаемая версия: " + expectedVersion + ", Текущая версия: " + existingEvents.Count);        }        existingEvents.AddRange(events);    }    public List<IEvent> GetEventsForAggregate(Guid aggregateId)    {        return _storage.ContainsKey(aggregateId) ? _storage[aggregateId] : new List<IEvent>();    }}

4. Выполнение событий и восстановление состояния

// Логика приложения (пример использования)var eventStore = new InMemoryEventStore();var accountId = Guid.NewGuid();var newAccount = BankAccount.CreateNew(accountId, "Иван Петров");newAccount.Deposit(100);newAccount.Withdraw(30);eventStore.SaveEvents(accountId, newAccount.GetUncommittedChanges(), -1);newAccount.MarkChangesAsCommitted();// Загрузить счет из другого места и выполнить операцииvar events = eventStore.GetEventsForAggregate(accountId);var existingAccount = new BankAccount();existingAccount.LoadFromHistory(events);Console.WriteLine($"Владелец счета: {existingAccount.Owner}, Баланс: {existingAccount.Balance}"); // Вывод: Владелец счета: Иван Петров, Баланс: 70existingAccount.Deposit(50);eventStore.SaveEvents(accountId, existingAccount.GetUncommittedChanges(), events.Count);existingAccount.MarkChangesAsCommitted();var updatedEvents = eventStore.GetEventsForAggregate(accountId);var updatedAccount = new BankAccount();updatedAccount.LoadFromHistory(updatedEvents);Console.WriteLine($"Обновленный владелец счета: {updatedAccount.Owner}, Обновленный баланс: {updatedAccount.Balance}"); // Вывод: Обновленный владелец счета: Иван Петров, Обновленный баланс: 120

Заключение

Event Sourcing предоставляет совершенно новую перспективу на архитектуру программного обеспечения. Сохраняя последовательность событий, которые составляют каждое состояние, а не просто снимки состояний, мы можем сделать наши системы более прозрачными, аудируемыми и гибкими. Хотя существует начальная кривая обучения, Event Sourcing предлагает мощный инструмент для разработчиков, особенно в сценариях сложной бизнес-логики, где исторические данные имеют решающее значение, и в микросервисной архитектуре. Этот паттерн позволяет понять не только 'как' выглядят ваши системы, но и 'почему' они выглядят именно так.

Назад к блогу

Комментарии (0)

Пока нет комментариев. Будьте первым!

Отправить комментарий