【发布时间】:2017-09-12 04:25:44
【问题描述】:
在许多不同的项目中,我看到了两种不同的引发领域事件的方法。
-
直接从聚合中引发领域事件。例如,假设您有 Customer 聚合,其中有一个方法:
public virtual void ChangeEmail(string email) { if(this.Email != email) { this.Email = email; DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail(email)); } }我可以看到这种方法存在 2 个问题。第一个是无论聚合是否持久化,都会引发事件。想象一下,如果您想在成功注册后向客户发送电子邮件。将引发事件“CustomerChangedEmail”,即使未保存聚合,某些 IEmailSender 也会发送电子邮件。当前实现的第二个问题是每个事件都应该是不可变的。所以问题是如何初始化它的“OccuredOn”属性?仅在内部聚合!这是合乎逻辑的,对!它迫使我将 ISystemClock (系统时间抽象)传递给聚合的每个方法!什么???你不觉得这种设计脆弱和笨重吗?以下是我们将提出的建议:
public virtual void ChangeEmail(string email, ISystemClock systemClock) { if(this.Email != email) { this.Email = email; DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail(email, systemClock.DateTimeNow)); } } -
第二种方法是按照事件溯源模式推荐的方式去做。在每个聚合上,我们定义了一个未提交事件的(列表)列表。请注意,UncommitedEvent 不是域事件!它甚至没有 OccuredOn 属性。现在,当在 Customer Aggregate 上调用 ChangeEmail 方法时,我们不会提出任何问题。我们只是将事件保存到我们聚合中存在的 uncommitedEvents 集合中。像这样:
public virtual void ChangeEmail(string email) { if(this.Email != email) { this.Email = email; UncommitedEvents.Add(new CustomerChangedEmail(email)); } }
那么,什么时候引发实际的域事件???这个责任被委托给持久层。在 ICustomerRepository 中,我们可以访问 ISystemClock,因为我们可以轻松地将它注入到存储库中。在 ICustomerRepository 的 Save() 方法中,我们应该从 Aggregate 中提取所有未提交的事件,并为每个事件创建一个 DomainEvent。然后我们在新创建的域事件上设置 OccuredOn 属性。然后,在一个事务中,我们保存聚合并发布所有域事件。通过这种方式,我们将确保所有事件都将在跨国界引发并具有总体持久性。
我不喜欢这种方法的什么?我不想为同一个事件创建 2 种不同的类型,即对于 CustomerChangedEmail 行为,我应该有 CustomerChangedEmailUncommited 类型和 CustomerChangedEmailDomainEvent。最好只有一种类型。请分享您对此主题的经验!
【问题讨论】:
-
这一切都很好,但恐怕这不是一个问题,所以它很可能会作为题外话被关闭。
-
问题是在持久化聚合之前发布域事件是否安全?
-
我已经看到即使是 Vaughn Vernon(著名的实现领域驱动设计的作者)也使用第一种方法,所以我想知道。也许我错过了smth,第一种方法是完全安全和好的?
-
第一种方法假定聚合在事务中被修改。因此,该事件立即引发,但仅在事务提交后处理。从技术上讲,可以将事件处理程序配置为仅在事务成功提交后触发。至于
OccuredOn属性 - 这可以由某种集中式事件工厂(甚至直接由域发布者)设置,因此您不必在每个聚合中复制代码。 -
@Mike Wojtyna,您能否提供一个示例,说明如何将事件处理程序配置为仅在事务提交成功后触发?不错的收获,但我没想到。关于“OccuredOn”,如果事件本身已经在聚合中创建并且它是不可变的,那么集中式事件工厂如何设置它?
标签: c# dns domain-driven-design