我在使用CQRS建模和实施事件考勤系统时遇到了问题。我的问题是一个子实体可以举起一个事件,但我不知道如何以及何时处理它。
基本上,一个事件可以让参与者以TBD状态开始,并且可以接受或拒绝参加该事件。但是,他们可以改变他们的出席率,当发生这种情况时,我希望引发一个事件,以便事件处理程序可以处理(例如,通知事件组织者)。
我已经使用状态模式来管理与会者的状态,并且它取决于当前状态是否应该引发事件。目前,此事件不会更改事件的状态。但是在我看来,这个事件应该是事件流的一部分。
我的问题是,我不知道是否会引发事件,直到我应用其中一个AttendeeResponded事件,该事件在当前状态下调用该方法。如果我在Apply期间提出事件,那么我会最终导致AR再水化问题。我可以在应用期间将此信息添加到事件中,具有状态返回信息,但随后事件变得可变。
我的想法是,状态模式可能不适合作为可以生成事件的地方,或者可能状态模式不适合这里。我可以扩展状态以使用一个方法来确定某个状态更改是否会抛出一个事件,但这看起来很笨拙。
最后,我的AR没有提及eventBus,所以我不能把一个活动扔到公交车上,而不是把它作为AR&的一部分#39; s事件流。我虽然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?
之前我有公共无效的AddAttendee(Guid memberID),但我将其切换到Member以尝试强制有效成员必须存在。我认为我这样做是错误的,并且自那时起创建了一个执行此验证的Attendance管理器并调用此方法。 (代码已更新以反映此情况)
我使用嵌套类来试图表明它是父子关系,但我同意,我不太喜欢它使Event类的大小。然而,AttendenceResponseState是嵌套的,因此它可以修改参加者的私有状态。你觉得这个用途有效吗? (更新了代码以将参加者移到Event类之外)
为了清楚起见,AttendenceResponseState是State Pattern的一个实现,而不是Attendee的完整状态(冲突的话:) :( / p>)
我同意Attendee并不真正需要成为一个实体,但ID来自我必须使用的另一个系统,所以我想我会在这里使用它。在编写SO代码时会丢失一些东西。
我个人不喜欢将聚合状态与聚合分开,而只是个人品味问题。如果我必须实施时刻,或者我获得更多经验,我可能会审查这个选择:)。 Ports和Sagas一样吗?
您能更多地谈谈聚合如何产生一个以上的事件吗?我认为这是我要做的事情之一。是否可以调用ApplyEvent,然后执行更多逻辑并可能再次调用ApplyEvent?
感谢您的意见,如果您有任何其他说明,我会很高兴听到您的意见。
答案 0 :(得分:3)
我会解决我不喜欢的事情。这并不意味着它是正确的做法。
public void AddAttendee(Member member)
IF成员是另一个聚合,我会用聚合ID而不是成员类型来引用它。 public void AddAttendee(MemberId member)
Aggregate应负责根据状态验证传入数据(状态是另一个没有逻辑的类,如DTO)。聚合还会创建和收集生成的新事件。操作完成后,命令处理程序将保留所有未提交的事件。如果操作成功,则发布事件。
请记住,一个命令必须只更新一个聚合并仅调用一个聚合方法,但更新聚合可能会产生一个或多个事件。
使所有投影都具有幂等性。
使用Ports(这些是处理来自当前有界上下文或其他有界上下文的事件并为当前有界上下文生成命令的事件处理程序)来更新多个聚合或处理来自其他有界上下文的事件。端口只能查询读取模型并生成命令,永远不会从那里更新读取模型。
我很少在模型中使用实体。我设计的几乎所有东西都是由聚合和值对象完成的。怪我:)。
可能这个答案不符合您的期望。我只是想分享一些对我有用的知识。我按照这些规则生产了2个系统。简单,减少错误。如果这些信息对您或其他人有任何价值,我将很高兴。
快乐编码
编辑:一些代码。请阅读评论。另外,我没有看到使用Attendee类的任何价值。更多信息请
public class Event : EventSourcedAggregateRoot<Guid>
{
private readonly HashSet<AttendeeId> _attendance = new HashSet<Attendee>();
private EventId _eventID;
private string _title;
// generating AR ID should not be a responsibility of the AR
// All my IDs are generated by the client or the place where commands are created
// One thing about building CQRS systems is the you must trust the client. This is important. Google it.
public Event(EventId id, LocalDate date, string title, List<AttendeeId> attendees/* Can you create an event without attendees? */)
{
HandleEvent(new EventCreated(date, title, attendees, id));
}
This override reminds me of an active record pattern.
//public override void Delete()
public void Cancel()
{
if (!Deleted)
HandleEvent(new EventDeleted(EventID));
}
// May be you could split it to two events. The other one could be RescheduleEvent
// and all attendees will be notified. But changing the title could be just a typo.
public void UpdateEvent(LocalDate date, string title)
{
HandleEvent(new EventUpdated(date, title, EventID));
}
public void AddAttendee(AttendeeId memberID)
{
Guard.That(() => _attendance).IsTrue(set => set.All(attendee => attendee.MemberID != memberID), "Attendee already exists");
HandleEvent(new AttendeeAdded(memberID, EventID));
}
public void DeleteAttendee(AttendeeId memberID)
{
Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
HandleEvent(new AttendeeDeleted(memberID, EventID));
}
internal void RespondIsComing(AttendeeId memberID)
{
Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
HandleEvent(new AttendeeRespondedAsComing(memberID, EventID));
}
internal void RespondNotComing(AttendeeId memberID)
{
Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
HandleEvent(new AttendeeRespondedAsNotComing(memberID, EventID));
}
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); // What this method does?
//attendee.Accept();
}
private void Apply(AttendeeRespondedAsNotComing @event)
{
var attendee = GetAttendee(@event.AttendeeID);// What this method does?
//attendee.Reject();
}
private void Apply(AttendeeDeleted @event)
{
_attendance.RemoveWhere(x => x.AttendeeID == @event.AttendeeID);
}
protected override void ApplyEvent(IAggregateEvent @event)
{
Apply((dynamic) @event);
}
}
回复=&gt; 回复mynkow的回复:
1)我会将我需要的所有信息从聚合状态复制到事件中并发布该事件。创建DTO并将它们存储在数据库中以提供UI的事件处理程序称为投影。您可以使用单词并将DTO称为投影。但这里的简单规则是:没有内部联合,没有从另一个表中选择。您只能从一个表中保存,选择和更新信息。
2)Guid工作了一段时间。使用AR类型真的很糟糕。创建一个表示AR ID的值对象。
3)只要只有聚合根关注包括相关实体在内的所有不变量,它才有效。
状态模式=&gt;不错。我使用相同的=&gt; https://github.com/Elders/Cronus/tree/master/Cronus.Persistence.MSSQL/src/Elders.Cronus.Sample.IdentityAndAccess/Accounts
实体与ValueObject =&gt;最好的例子。当我教juniors =&gt;时,我总是使用它http://lostechies.com/joeocampo/2007/04/15/a-discussion-on-domain-driven-design-entities/
想象一下,客户从电子商务网站购买了一些东西。他每个月花100美元。你可以有一个规则,如果你有10个conseq。购买月份&gt; 100美元您将礼物附加到客户订单。这是你可以举办超过1场比赛的方式。而这正是有趣的东西实际存在的地方。 ;)