【问题标题】:Handling Domain events within a single aggregate?在单个聚合中处理域事件?
【发布时间】:2020-07-17 20:44:05
【问题描述】:

我正在研究领域驱动设计的实现,其中我们在同一个聚合中的一些操作需要与其他操作一起发生。这两个操作彼此无关。

这是一个示例代码。

请注意,此代码仅用于说明目的,并非真实示例。

public ProcessOrderCommandHandler : IHandler<ProcessOrderCommand>
{
     public async Task Handle(ProcessOrderCommand orderCommand)
     {
            var order = _repository.LoadOrder(orderCommand.Id);

            // Operation - 1
            order.AddToCart(orderCommand.Item);

            // Operation - 2
            order.ProcessOrder();
     }
}

public class Order : Aggregate
{
     public void AddToCart(Item item)
     { ... }

     public void ProcessOrder()
     { ... }       
}

ProcessOrderAddToCart 操作不相关,我有很多这样的CommandHandlers,它们相互独立,但仍需要结合调用。

我看到有三个选项可以解决这个问题:

  • 选项1:以上示例代码。

    我不是特别喜欢这个选项,因为我们需要在单个 CommandHandler 中调用多个域操作。

  • 选项 2:更新域操作以在单个方法调用中执行两个操作,如下所示

    public ProcessOrderCommandHandler: IHandler<ProcessOrderCommand>
    {
         public async Task Handle(ProcessOrderCommand orderCommand)
         {
                var order = _repository.LoadOrder(orderCommand.Id);
    
                order.AddToCartAndProcessOrder(orderCommand.Item);
         }
    }
    
    public class Order : Aggregate
    {
         public void AddToCartAndProcessOrder(Item item)
         { 
              AddToCart(item);
              ProcessOrder();
         }
    
         private void AddToCart(Item item)
         { ... }       
    
         private void ProcessOrder()
         { ... }       
    }
    

同样,如果这是正确的方法,也不是 100% 方便,因为我需要在所有相关操作中这样做。

  • 选项 3:引发和处理域事件

    public ProcessOrderCommandHandler : IHandler<ProcessOrderCommand>
    {
         public async Task Handle(ProcessOrderCommand orderCommand)
         {
              var order = _repository.LoadOrder(orderCommand.Id);
    
              // Operation - 1
              order.AddToCart(orderCommand.Item);
         }
     }
    
    
     public class Order : Aggregate
     {
         public void AddToCart(Item item)
         {
             ...
             AddDomainEvent(new OrderUpdated(id));
         }
    
         public void ProcessOrder()
         { ... }       
     }
    
     public OrderUpdatedEventHandler : IDomainEventHandler<OrderUpdatedEvent>
     {
         public async Task Handle(OrderUpdatedEvent orderUpdatedEvent)
         {
              //  Loads the same order object from Cache
              var order = _repository.LoadOrder(orderUpdatedEvent.Id);
    
              // Operation - 2
              order.ProcessOrder();
         }
     }
    

我觉得这种方法是所有方法中最干净的,因为它有助于保持关注点分离。 但是,在这里,我通过 Domain-Event 处理同一聚合中的两个域操作,这不是通常使用 Domain-Events 的方式。根据来自 Microsoft Docs 的域事件definition

使用领域事件显式实现更改的副作用 在您的域内。

换句话说,使用 DDD 术语,使用领域事件显式地 跨多个聚合实现副作用。

问题:选项 3 是领域驱动设计中可接受的解决方案吗?

  • 如果是,您能否分享一些在同一聚合中处理领域事件的参考资料/链接?

  • 如果没有,我还有哪些其他选择,包括但不限于选项 1 和 2?

【问题讨论】:

  • 我不同意这个定义。领域事件没有任何迹象表明它们不应该完全在您的领域模型中使用,甚至不应该在单个聚合中使用。

标签: c# domain-driven-design


【解决方案1】:

ProcessOrderAddToCart 操作不相关,我有很多这样的CommandHandlers,它们相互独立,但仍需要结合调用。

在直接回答您的问题之前,我建议重新评估此陈述。如果操作彼此不相关,为什么它们属于同一个聚合?另外,这么多CommandHandlers 是否在同一个聚合上运行?

简而言之,您确定您的聚合是正确的吗?他们应该有一个单一而明确的责任。如果有不相关的操作或过多的操作,他们可能会承担多个职责。

现在,关于您的三个选项。在我看来CommandHandlers 就是所谓的Use Cases in Clean Architecture。在您的场景中,一个用例基本上从数据库加载聚合,调用业务操作,并将更新的聚合存储回数据库(加上潜在的发布事件、与第三方交谈等)。因此,如果您这样看,您的用例需要在同一个聚合中调用 2 个业务操作这一事实不是问题,因为这是您的用例指定的。

如果这两个操作分别没有意义,您可以将这两个操作合二为一。

如果我们假设聚合是正确的,那么选项 3 是唯一一个我不会使用的选项。在我看来,这将是滥用模式来处理不应该使用的东西。

但是,如果您认为这些操作实际上属于不同的聚合,那么使用事件肯定是最好的选择。

考虑以下要求:

  1. 当用户点击添加到购物车时,应将所选产品添加到购物车中
  2. 将产品添加到购物车后,应重新计算运费

在这种情况下,您显然有两个聚合(购物车和运输)。添加到购物车和重新计算运费是两个独立的操作,将它们与事件协调起来更有意义,并且符合业务规范(“当 X 发生时......”)。

现在考虑前面示例的另一个用例:

  1. 收货地址更新后,需重新计算运费

现在,更新送货地址和重新计算运费是对同一个聚合的两个操作,但在这种情况下,将它们与用例分开调用是没有意义的,因为在不重新计算费用的情况下更改地址会使聚合处于不一致的状态,因此聚合本身可以在地址更改后立即自动重新计算成本。

【讨论】:

  • > 简而言之,您确定您的聚合是正确的吗?是的,聚合是正确的,它们有一个单一的责任。我在这里的示例仅用于说明目的,因此可能会引起混淆。聚合上的多个操作并不总是意味着它们需要在我的书中拆分。但我明白你来自哪里。
  • 好的,那么我的部分答案仍然适用。这取决于您的用例以及这些操作是否也作为单独的操作有意义。
【解决方案2】:

ProcessOrder() 方法是否仅反映应在同一聚合上执行的聚合内的更改,以对将某些商品添加到购物车作出反应?如果是,我会将对该操作的调用移到 AddToCart() 方法中。如果在添加项目时必须在同一聚合中发生某些业务不变性,为什么要把该责任放在应用程序层上?这只会让您的业务逻辑泄漏到域层之外。如果您想使用基于事件的方法在同一个聚合上内聚地实现这些操作,那么您选择的设计决策可能更适合也可能不适合。但是,添加项目后必须进行此处理不再是应用层的责任。

但是,如果 ProcessOrder() 方法代表用户在购物车中添加了一些东西(比如提交订单)之后工作流程的下一步,你应该问自己,是我的交互域模型的设计过于基于 CRUD

所以根据我的经验,DDD 通常更适合,或者说更适用于基于任务的用户界面。这意味着客户端(例如 Web 浏览器)通过执行较小的定义明确的任务与系统交互,而不是发送大量数据,然后将其转换为在域模型(此处为聚合)上按顺序执行的多个任务。

因此,在您的示例中,如果 ProcessOrder() 表示“提交订单”,我个人将与后端进行两次交互。一个用于向购物车添加东西 - 用例 A - 第二个用于提交订单 - 用例 B。

对我来说,在这种情况下,从客户的角度来看,这也感觉更自然,因为我希望在单独的步骤中执行这些任务。此外,如果您考虑使用这种方法,则不必处理事件,尤其是在对同一聚合进行操作时。

【讨论】:

    【解决方案3】:

    如果没有太多的领域背景,很难判断,但这是您应该从产品专家那里得到的答案。

    您似乎有一个 Order 聚合,它公开了一个公共接口来对购物车进行操作。

    1. 此聚合真的是 OrderAggregate 还是 ShoppingCartAggregate?
    2. 我假设“处理订单”实际上是指下订单(在我看来,作为此操作的副作用,我看到付款发生、库存管理等等)。如果是这种情况,您似乎可以通过以下步骤来改进您的域设计:
    • 买家在购物车中添加产品
    • 买家前往收银台并检查购物车中的产品
    • 买家付款
    • 买家收到购买收据

    (这是对任何商店中实际发生的事情的描述,以帮助说明我的想法)

    在这个过程中我们有:

    • 买方是在用例中演变的角色
    • 购物车,即买家有购买意向的产品列表。但由于它们代表一种意图,因此放置或移除物品非常容易
    • 结帐购物车中的商品实际上是对购物车的最后状态进行快照
    • 作为结帐的结果(我假设在您的情况下是流程订单),系统会提示买家付款以及结帐后可能发生的任何其他副作用。

    同样,这是我根据您的描述做出的假设,但我用它来表明可以从业务流程中实际发生的情况中提取概念,并且领域专家应该帮助您正确处理它们。

    【讨论】:

      猜你喜欢
      • 2021-07-22
      • 2019-02-26
      • 1970-01-01
      • 2015-01-08
      • 1970-01-01
      • 2021-12-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多