领域事件如何从领域对象内部分发?

6

领域对象不应该有任何依赖性,因此也不需要进行依赖注入。然而,当在领域对象内部分派领域事件时,我可能希望使用集中式的 EventDispatcher。那么我如何获取一个呢?

我不想将事件列表返回给调用者,因为我希望它们保持不透明并保证它们的分派。这些事件只应由其他需要执行最终一致性约束的领域对象和服务来消耗。

3个回答

2

参见Udi Dahan的领域事件

基本上,您需要为您的领域事件注册一个或多个处理程序,然后像这样引发事件:

public class Customer
{
   public void DoSomething()
   {
      DomainEvents.Raise(new CustomerBecamePreferred() { Customer = this });
   }
}

所有已注册的处理程序将被执行:

public void DoSomethingShouldMakeCustomerPreferred()
{
   var c = new Customer();
   Customer preferred = null;

   DomainEvents.Register<CustomerBecamePreferred>(p => preferred = p.Customer);

   c.DoSomething();
   Assert(preferred == c && c.IsPreferred);
}

这基本上是实现了Hollywood Principle (别打电话给我们,我们会打给你),因为你不直接调用事件处理程序,而是在事件被触发时执行事件处理程序。


1
这正是我一直在寻找的。最值得注意的是,在这种情况下,静态引用是可以的,并且不会妨碍可测试性,就像链接的文章中所解释的那样。 - Double M
1
静态的DomainEvents是一种环境上下文的形式。这是一种反模式,因此Udi Dahan早就放弃了使用这个静态类来发布事件。你应该更倾向于@VoiceOfUnreason的答案。 - Steven
1
@Hooman:我不确定Udi是否有一篇完整的博客介绍这个问题。我已经搜索了一个小时,但是找不到来源。所以,很遗憾,我无法证明这一点。他在2012年或更早的时候发表了这个声明。希望其他人能够提供给我们一个链接。然而,在DI PP&P的第5.3章中有一个长描述,解释了为什么环境上下文是一种反模式。 - Steven
1
这不是我所说的评论,因为他并没有谈论这个静态类的用法。Saga是流程管理器模式的另一个词,它是处理领域事件的协调者。因此,他并没有反对发布领域事件。 - Steven
我在Udi Dahan的评论中发现了一条关于领域事件的信息,他在这个页面的底部第210个评论中说:“现在,我不再推荐使用领域事件,而是倾向于使用NServiceBus来建模事物。我发现这样可以大大简化事情-有效地将saga转换为领域模型模式的一个实例,在这种情况下,事件就成为正常的NServiceBus事件。” - Hooman Bahreini

2
我可能会想使用一个集中式的EventDispatcher。我该如何获取一个?
将其作为参数传递。
它可能看起来不像一个EventDispatcher,而是像一些描述特定领域所需能力的域服务。在组合应用程序时,您可以选择使用哪个服务实现。

1
将它作为参数传递到哪里?如果你的意思是每个AR方法都要传递一个Dispatcher,那么这似乎非常繁琐。我更喜欢一个静态实例或者像在ES中那样简单地记录AR上的事件。 - plalx

1
你在要求两全其美。你需要注入依赖项或反转控制并让另一个对象管理Aggregate和EventDispatcher之间的交互。我建议尽可能保持聚合简单,以使它们不受依赖关系的影响,并保持可测试性。
以下代码示例非常简单,并且不适用于生产环境,但说明如何设计无依赖关系的聚合,而无需在需要它们的上下文之外传递事件列表。
如果您的聚合中有事件列表:
class MyAggregate 
{
    private List<IEvent> events = new List<IEvent>();

    // ... Constructor and event sourcing?

    public IEnumerable<IEvent> Events => events;

    public string Name { get; private set; }

    public void ChangeName(string name)
    {
        if (Name != name) 
        { 
            events.Add(new NameChanged(name); 
        }
    }
}

然后你可能会有一个类似这样的处理程序:

public class MyHandler 
{
    private Repository repository;

    // ... Constructor and dependency injection

    public void Handle(object id, ChangeName cmd)
    {
        var agg = repository.Load(id);
        agg.ChangeName(cmd.Name);
        repository.Save(agg);
    }
}

一个看起来像这样的代码仓库:

class Repository
{
    private EventDispatcher dispatcher;

    // ... Constructor and dependency injection

    public void Save(MyAggregate agg)
    {
        foreach (var e in agg.Events)
        {
            dispatcher.Dispatch(e);                
        }
    }
}

嗯...所以我需要保持一个“public”列表,记录事务中发生的所有事件。通常是这样做的吗? - Double M
@DoubleM 这是由特定聚合生成的事件列表。这是我采用的方法。我能够在没有EventDispatcher概念的情况下测试聚合。简单的域逻辑。或者,您可以从聚合中引发事件,并让存储库订阅并在内部保留列表。想法是使聚合不受依赖项的限制。更进一步,这些事件不会在事务范围内分派。它们被写入EventStore,然后从那里读取,并由独立的进程分派。 - CPerson
“你所说的‘你要么需要注入依赖项,要么反转控制’是什么意思?” [DI是IoC的一种形式] (https://dev59.com/Umw15IYBdhLWcg3wisS-) - Hooman Bahreini
@Hooman 我的意思是,与其在聚合内部具有调度事件的逻辑,不如让存储库驱动该逻辑。依赖注入是IoC的一种形式,但不是唯一的形式。 - CPerson
1
@DoubleM 我认为Udi所展示的是你可以绕过静态引用的限制,只要你记得清除全局状态。我不喜欢将我的领域与特定商业供应商绑定在一起,特别是当避免这种锁定的成本很低而消除它的价格很高时(在一个大型和完善的领域中)。此外,你的领域应该是一个纯业务规则引擎还是一个消息分发层? - CPerson
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接