Event Sourcing
Перейти до навігації
Перейти до пошуку
Event Sourcing — це шаблон проєктування, який представляє стан об'єкта у вигляді множини подій.
Потрібно мати доступ до історії змін сутності.
- доступ до історії змін сутності
- можливість відновити стан на момент часу
- важкий в реалізації
- важко опрацювати зміну структури моделі
- поганий коли часто потрібно знати стан сутності
- Події не можна видаляти чи змінювати. Будь-який новий стан сутності потрібно змінювати шляхом додавання нової події.
- Для того щоб покращити продуктивність і не застосовувати всі події вводять термін snapshot. Це збережений стан моделі із врахуванням подій на момент часу. Існують різноманітні стратегії додавання snapshot'ів, від кількості подій до часового проміжку між ними.
Додамо деякі класи, які будуть симулювати реальні об'єкти.
public class User
{
public int Id { get; internal set; }
public string Name { get; internal set; }
public override string ToString()
{
return $"User = {Id} - {Name}";
}
}
Додамо базовий клас для подій
public abstract class EventBase
{
public int Id { get; }
public EventBase(int entityId)
{
Id = entityId;
}
public abstract void Apply(User entity);
}
Та декілька реалізацій цих подій:
public class UserCreatedEvent : EventBase
{
private readonly int id;
private readonly string name;
public UserCreatedEvent(int id, string name)
: base(id)
{
this.id = id;
this.name = name;
}
public override void Apply(User entity)
{
entity.Id = id;
entity.Name = name;
}
}
public class UserNameChangedEvent : EventBase
{
private readonly string name;
public UserNameChangedEvent(int id, string name)
: base(id)
{
this.name = name;
}
public override void Apply(User entity)
{
entity.Name = name;
}
}
Змінимо нашу сутність таким чином, щоб вона містила інформацію про події:
public class User
{
...
internal User() { }
public User(int id, string name)
{
Id = id;
Name = name;
events.Add(new UserCreatedEvent(id, name));
}
public void SetName(string name)
{
Name = name;
events.Add(new UserNameChangedEvent(this.Id, name));
}
private ICollection<EventBase> events = new List<EventBase>();
public IEnumerable<EventBase> Events => events;
}
Та сховище, яке вміє працювати із нашою сутністю та подіями:
public class Store
{
private readonly ICollection<EventBase> events = new List<EventBase>();
public void Add(User user)
{
foreach (var domainEvent in user.Events)
{
events.Add(domainEvent);
}
}
public User GetById(int id)
{
var domainEvents = events.Where(e => e.Id == id);
var user = new User();
foreach (var domainEvent in domainEvents)
{
domainEvent.Apply(user);
}
return user;
}
}
Використання цього шаблону не помітне для користувачів:
class Program
{
static void Main(string[] args)
{
var store = new Store();
var user = new User(id: 1, name: "John");
user.SetName("Jane");
store.Add(user);
var userFromStore = store.GetById(1);
Console.WriteLine(userFromStore);
}
}
Приклад реалізації на мові С#
using System;
using System.Linq;
using System.Collections.Generic;
namespace EventSourcing
{
public abstract class EventBase
{
public int Id { get; }
public EventBase(int entityId)
{
Id = entityId;
}
public abstract void Affect(User entity);
}
public class User
{
public int Id { get; private set; }
public string Name { get; private set; }
public override string ToString()
{
return $"User = {Id} - {Name}";
}
internal User() { }
public User(int id, string name)
{
Apply(new UserCreatedEvent(id, name));
}
public void SetName(string name)
{
Apply(new UserNameChangedEvent(this.Id, name));
}
public void Apply(UserCreatedEvent userCreatedEvent)
{
this.Id = userCreatedEvent.Id;
this.Name = userCreatedEvent.Name;
events.Add(userCreatedEvent);
}
public void Apply(UserNameChangedEvent userNameChangedEvent)
{
this.Name = userNameChangedEvent.Name;
events.Add(userNameChangedEvent);
}
private ICollection<EventBase> events = new List<EventBase>();
public IEnumerable<EventBase> Events => events;
public void ClearEvents()
{
this.events.Clear();
}
}
public class UserCreatedEvent : EventBase
{
public int Id { get; }
public string Name { get; }
public UserCreatedEvent(int id, string name)
: base(id)
{
this.Id = id;
this.Name = name;
}
public override void Affect(User entity)
{
entity.Apply(this);
}
}
public class UserNameChangedEvent : EventBase
{
public string Name { get; }
public UserNameChangedEvent(int id, string name)
: base(id)
{
this.Name = name;
}
public override void Affect(User entity)
{
entity.Apply(this);
}
}
public class Store
{
private readonly ICollection<EventBase> events = new List<EventBase>();
public void Add(User user)
{
foreach (var domainEvent in user.Events)
{
events.Add(domainEvent);
}
}
public User GetById(int id)
{
var domainEvents = events.Where(e => e.Id == id);
var user = new User();
foreach (var domainEvent in domainEvents)
{
domainEvent.Affect(user);
}
user.ClearEvents();
return user;
}
}
class Program
{
static void Main(string[] args)
{
var store = new Store();
var user = new User(id: 1, name: "John");
user.SetName("Jane");
store.Add(user);
var userFromStore = store.GetById(1);
Console.WriteLine(userFromStore);
}
}
}
- A simple snapshots example [Архівовано 9 березня 2020 у Wayback Machine.]
- Event sourcing: handle event schema changing [Архівовано 24 липня 2021 у Wayback Machine.]