У меня возникла проблема с моделированием и внедрением системы посещаемости мероприятий с использованием CQRS. Моя проблема в том, что дочерняя сущность может вызвать событие, но я не уверен, как и когда его обрабатывать.
По сути, у мероприятия могут быть участники, которые начинают в состоянии TBD, и могут принять или отклонить участие в мероприятии. Однако они могут изменить свою посещаемость, и когда это произойдет, я хотел бы, чтобы событие было инициировано, чтобы обработчик событий мог обработать (например, уведомить организатора мероприятия).
Я использовал шаблон состояния для управления состоянием посетителя, и от текущего состояния зависит, должно ли быть инициировано событие. На данный момент это событие не меняет состояние События. Однако мне кажется, что это событие должно быть частью потока событий.
Моя проблема в том, что я не знаю, возникнет ли событие, пока я не применю одно из событий AttendeeResponded, которое вызывает метод в текущем состоянии. Если я инициирую событие во время применения Apply, у меня возникнет проблема регидратации. AR. Я мог бы добавить эту информацию к событию во время применения, имея возвращаемую информацию о состоянии, но тогда событие стало бы изменяемым.
Я думаю, что, возможно, шаблон состояния не работает как место, где могут быть сгенерированы события, или что, возможно, шаблон состояния здесь не подходит. Я мог бы расширить состояние, чтобы иметь метод, который определяет, вызовет ли определенное изменение состояния событие, но это кажется неуклюжим.
Наконец, в моем AR нет ссылок на eventBus, поэтому я не могу просто передать событие на шину и не включить его в поток событий AR. Мне казалось, что AR, имеющая ссылку на шину событий, начинала нарушать SRP, но, возможно, я ошибаюсь в этом.
Я включил упрощенный код, чтобы помочь моему описанию. Кто-нибудь может дать полезные советы? Спасибо, Фил
public class Event : EventSourcedAggregateRoot<Guid>
{
#region Fields
private readonly HashSet<Attendee> _attendance = new HashSet<Attendee>();
private Guid _eventID;
private string _title;
#endregion
#region Constructors
[Obsolete]
private Event()
{
}
public Event(LocalDate date, string title)
{
HandleEvent(new EventCreated(date, title, new GuidCombGenerator().GenerateNewId()));
}
public Event(IEnumerable<IAggregateEvent<Guid>> @events)
{
LoadsFromHistory(@events);
}
#endregion
#region Properties and Indexers
public IReadOnlyCollection<Attendee> Attendance
{
get { return _attendance.ToArray(); }
}
public Guid EventID
{
get { return _eventID; }
private set
{
if (_eventID == new Guid()) _eventID = value;
else throw new FieldAccessException("Cannot change the ID of an entity.");
}
}
public LocalDate Date { get; private set; }
public override Guid ID
{
get { return EventID; }
set { EventID = value; }
}
public string Title
{
get { return _title; }
private set
{
Guard.That(() => value).IsNotNullOrWhiteSpace();
_title = value;
}
}
#endregion
#region Methods
public override void Delete()
{
if (!Deleted)
HandleEvent(new EventDeleted(EventID));
}
public void UpdateEvent(LocalDate date, string title)
{
HandleEvent(new EventUpdated(date, title, EventID));
}
public void AddAttendee(Guid memberID)
{
Guard.That(() => _attendance).IsTrue(set => set.All(attendee => attendee.MemberID != memberID), "Attendee already exists");
HandleEvent(new AttendeeAdded(memberID, EventID));
}
public void DeleteAttendee(Guid memberID)
{
Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
HandleEvent(new AttendeeDeleted(memberID, EventID));
}
internal void RespondIsComing(Guid memberID)
{
Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
HandleEvent(new AttendeeRespondedAsComing(memberID, EventID));
}
internal void RespondNotComing(Guid memberID)
{
Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
HandleEvent(new AttendeeRespondedAsNotComing(memberID, EventID));
}
#endregion
#region Event Handlers
private void Apply(EventCreated @event)
{
Date = @event.Date;
Title = @event.Title;
EventID = @event.EventID;
}
private void Apply(EventDeleted @event)
{
Deleted = true;
}
private void Apply(AttendeeAdded @event)
{
_attendance.Add(new Attendee(@event.MemberID, @event.EventID));
}
private void Apply(EventUpdated @event)
{
Title = @event.Title;
Date = @event.Date;
}
private void Apply(AttendeeRespondedAsComing @event)
{
var attendee = GetAttendee(@event.AttendeeID);
attendee.Accept();
}
private void Apply(AttendeeRespondedAsNotComing @event)
{
var attendee = GetAttendee(@event.AttendeeID);
attendee.Reject();
}
private void Apply(AttendeeDeleted @event)
{
_attendance.RemoveWhere(x => x.AttendeeID == @event.AttendeeID);
}
protected override void ApplyEvent(IAggregateEvent @event)
{
Apply((dynamic) @event);
}
#endregion
}
public class Attendee
{
#region AttendenceResponse enum
public enum AttendenceResponse
{
TBD,
Coming,
NotComing
}
#endregion
#region Fields
private IAttendenceResponseState _attendState;
private readonly Guid _eventID;
private readonly Guid _memberID;
#endregion
#region Constructors
public Attendee(Guid memberID, Guid EventID)
{
_memberID = memberID;
_eventID = EventID;
_attendState = new TBD(this);
}
#endregion
#region Properties and Indexers
public IAttendenceResponseState AttendingState
{
get { return _attendState; }
private set { _attendState = value; }
}
public Guid EventID
{
get { return _eventID; }
}
public Guid MemberID
{
get { return _memberID; }
}
#endregion
#region Methods
public void Accept()
{
_attendState.Accept();
}
public void Reject()
{
_attendState.Reject();
}
#endregion
#region Nested type: IAttendenceResponseState
public interface IAttendenceResponseState
{
#region Properties and Indexers
AttendenceResponse AttendenceResponse { get; }
#endregion
#region Methods
void Accept();
void Reject();
#endregion
}
#endregion
#region Nested type: Coming
private class Coming : IAttendenceResponseState
{
#region Fields
private readonly Attendee _attendee;
#endregion
#region Constructors
public Coming(Attendee attendee)
{
_attendee = attendee;
}
#endregion
#region IAttendenceResponseState Members
public void Accept()
{
}
public AttendenceResponse AttendenceResponse
{
get { return AttendenceResponse.Coming; }
}
public void Reject()
{
_attendee.AttendingState = (new NotComing(_attendee));
//Here is where I would like to 'raise' an event
}
#endregion
}
#endregion
#region Nested type: NotComing
private class NotComing : IAttendenceResponseState
{
#region Fields
private readonly Attendee _attendee;
#endregion
#region Constructors
public NotComing(Attendee attendee)
{
_attendee = attendee;
}
#endregion
#region IAttendenceResponseState Members
public void Accept()
{
_attendee.AttendingState = (new Coming(_attendee));
//Here is where I would like to 'raise' an event
}
public AttendenceResponse AttendenceResponse
{
get { return AttendenceResponse.NotComing; }
}
public void Reject()
{
}
#endregion
}
#endregion
#region Nested type: TBD
private class TBD : IAttendenceResponseState
{
#region Fields
private readonly Attendee _attendee;
#endregion
#region Constructors
public TBD(Attendee attendee)
{
_attendee = attendee;
}
#endregion
#region IAttendenceResponseState Members
public void Accept()
{
_attendee.AttendingState = (new Coming(_attendee));
}
public AttendenceResponse AttendenceResponse
{
get { return AttendenceResponse.TBD; }
}
public void Reject()
{
_attendee.AttendingState = (new NotComing(_attendee));
}
#endregion
}
#endregion
}
Ответ на ответ mynkow:
Я показываю некоторые состояния (только для чтения), чтобы я мог создавать прогнозы текущего состояния агрегата. Как бы вы обычно это делали? Вы создаете проекцию непосредственно из событий (это кажется более сложным, чем чтение текущего состояния из агрегата) или у вас есть агрегат для создания DTO?
Раньше у меня был public void AddAttendee (Guid memberID), но я переключаю его на Member, чтобы попытаться заставить существовать действительный член. Я думаю, что ошибался, делая это, и с тех пор создал диспетчера посещаемости, который выполняет эту проверку и вызывает этот метод. (код обновлен, чтобы отразить это)
Я использовал вложенные классы, чтобы попытаться показать, что это отношения родитель-потомок, но я согласен, мне не очень нравится, насколько большим он делает класс Event. Однако AttendenceResponseState является вложенным, так что он может изменять частное состояние посетителя. Как вы думаете, такое использование допустимо? (код обновлен, чтобы переместить участника за пределы класса события)
Чтобы быть ясным, AttendenceResponseState - это реализация шаблона состояния, а не полное состояние участника (противоречивые слова :))
И я согласен с тем, что Attendee на самом деле не обязательно должен быть объектом, но идентификатор получен из другой системы, с которой мне нужно работать, поэтому я подумал, что буду использовать его здесь. Некоторые вещи теряются при подготовке кода для SO.
Мне лично не нравится отделять агрегатное состояние от агрегированного, это просто вопрос личного вкуса. Я мог бы пересмотреть этот выбор, если мне нужно будет реализовать momento или по мере того, как я наберусь опыта :). Также порты такие же, как и саги?
Не могли бы вы подробнее рассказать о том, как агрегат может производить более одного события? Думаю, это одна из вещей, которую я пытаюсь сделать. Можно ли вызвать ApplyEvent, затем выполнить дополнительную логику и, возможно, вызвать ApplyEvent во второй раз?
Спасибо за ваш вклад, и если у вас есть другие заметки, я буду рад их выслушать.