Ответ
Event Sourcing (Источники событий) — это архитектурный паттерн, при котором состояние приложения определяется не как последний «снимок» данных, а как последовательность всех событий (история изменений), которые привели к этому состоянию. Вместо обновления записи в БД каждое изменение сохраняется как неизменяемое событие (event). Текущее состояние вычисляется путём применения (проецирования) всей цепочки событий.
Ключевые принципы:
- Неизменяемость событий: События, будучи сохранёнными, никогда не изменяются и не удаляются.
- Единственный источник истины: Журнал событий (event log) является первичным и наиболее достоверным хранилищем данных.
- Воспроизводимость: Любое состояние может быть восстановлено «с нуля» путём повторного применения событий, что обеспечивает мощные возможности для отладки, аудита и миграции.
Преимущества:
- Полный аудит: Вся история изменений доступна по умолчанию.
- Временные запросы: Можно «отмотать» состояние приложения к любому моменту в прошлом.
- Гибкость: Новые представления данных (проекции) могут быть созданы из существующего потока событий без изменения основной логики.
- Согласованность: Упрощает реализацию сложных бизнес-транзакций и реакцию на изменения.
Недостатки и сложности:
- Сложность запросов: Прямые запросы к журналу событий неэффективны. Паттерн почти всегда используется в связке с CQRS (Command Query Responsibility Segregation), где для чтений создаются оптимизированные проекции (read models).
- Рост данных: Требует стратегий для управления постоянно растущим журналом событий (снепшоты, архивация).
- Сложность освоения: Требует сдвига парадигмы в мышлении о данных.
Пример на C# (Упрощённая модель банковского счёта):
// 1. Определяем события (неизменяемые объекты-записи)
public abstract record AccountEvent(Guid AccountId, DateTime OccurredAt);
public record AccountOpened(Guid AccountId, string Owner, DateTime OccurredAt) : AccountEvent(AccountId, OccurredAt);
public record MoneyDeposited(Guid AccountId, decimal Amount, DateTime OccurredAt) : AccountEvent(AccountId, OccurredAt);
public record MoneyWithdrawn(Guid AccountId, decimal Amount, DateTime OccurredAt) : AccountEvent(AccountId, OccurredAt);
// 2. Агрегат, который обрабатывает команды и генерирует события
public class BankAccount
{
public Guid Id { get; private set; }
public string Owner { get; private set; } = string.Empty;
public decimal Balance { get; private set; }
private readonly List<AccountEvent> _pendingEvents = new();
// Восстановление состояния из истории событий
public BankAccount(IEnumerable<AccountEvent> pastEvents)
{
foreach (var evt in pastEvents)
Apply(evt);
}
// Метод для применения события (изменяет состояние)
private void Apply(AccountEvent evt)
{
switch (evt)
{
case AccountOpened opened:
Id = opened.AccountId;
Owner = opened.Owner;
Balance = 0m;
break;
case MoneyDeposited deposited:
Balance += deposited.Amount;
break;
case MoneyWithdrawn withdrawn:
Balance -= withdrawn.Amount;
break;
}
}
// Команды: проверяют бизнес-правила и создают новые события
public void Open(string owner)
{
if (Id != Guid.Empty) throw new InvalidOperationException("Account already open.");
var evt = new AccountOpened(Guid.NewGuid(), owner, DateTime.UtcNow);
_pendingEvents.Add(evt);
Apply(evt);
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Deposit amount must be positive.");
var evt = new MoneyDeposited(Id, amount, DateTime.UtcNow);
_pendingEvents.Add(evt);
Apply(evt);
}
// Возвращает новые, ещё не сохранённые события
public IReadOnlyList<AccountEvent> GetPendingEvents() => _pendingEvents.AsReadOnly();
public void ClearPendingEvents() => _pendingEvents.Clear();
}
// 3. Использование
// Восстановление счёта из хранилища событий
var pastEvents = eventStore.LoadEvents(accountId);
var account = new BankAccount(pastEvents);
// Выполнение новой команды
account.Deposit(100);
// Сохранение новых событий
var newEvents = account.GetPendingEvents();
eventStore.SaveEvents(account.Id, newEvents);
account.ClearPendingEvents();