DDD - 聚合内子对象的修改

16

我在处理一个相当复杂的情况时遇到了一些困难。我看到过很多类似的问题,但没有一个能满足我的需求。

一个订单(聚合根)会创建多个订单行(子实体)。根据业务规则,每个订单行必须在整个订单生命周期内保持相同的标识。订单行有许多(20+)属性,并且在订单被“锁定”之前可能经常发生变化。此外,在根级别必须执行不变量;例如,每个订单行都有一个数量,订单的总数量不能超过X。

当考虑对订单行进行更改时,我不确定如何对此场景进行建模。我想到了4种选择,但似乎都不太令人满意:

1) 当需要修改订单行时,请使用根提供的引用进行修改。但我失去了在根中检查不变逻辑的能力。

var orderLine = order.GetOrderLine(id);
orderLine.Quantity = 6;

2) 在订单上调用一个方法。我可以应用所有不变逻辑,但是我会被卡在需要修改众多OrderLine属性的方法泛滥中:

order.UpdateOrderLineQuantity(id, 6);
order.UpdateOrderLineDescription(id, description);
order.UpdateOrderLineProduct(id, product);
...

3) 如果我将OrderLine视为值对象可能会更容易,但根据业务需求,它必须保持相同的身份。

4) 我可以获取对OrderLines的引用以进行不影响不变量的修改,并针对那些影响不变量的修改遍历Order。但如果大多数OrderLine属性都会影响不变量怎么办?这个反驳是假设性的,因为只有少数属性会影响不变量,但是随着我们发现更多的业务逻辑,情况可能会改变。

非常感谢您给出的任何建议...请毫不犹豫地告诉我是否理解得不够透彻。

6个回答

9
  1. 这种方法不是最优的,因为它容易破坏域不变性。

  2. 会导致代码重复和不必要的方法爆炸。

  3. 与1)相同。使用值对象无法帮助保持领域不变性。

  4. 我会选择此选项。我也不会担心可能的和假设性变化,直到它们真正发生。设计将随着您对领域的理解而发展,并且随时可以进行重构。阻碍现有设计以追求未来可能不会发生的变化实际上没有任何价值。


谢谢你的回答 - 我认为这可能是我提出的选择中最好的。实际上,我希望有人能够建议我可能忽略的更好的模式 ;) 我有点不敢使用它,因为它不够干净。或者说,不够一致,正如 @eulerfx 指出的那样。但我想现在它还可以... - Cork
2
我知道我已经接受了这个答案...但我想到了一个不同的方法。在订单上有一个Save(IOrderLine)方法可以吗?然后我可以传递对订单的引用,避免一堆细粒度的方法,仍然允许订单强制执行不变量。 - Cork

8
相对于2,4的一个缺点是缺乏一致性。在某些情况下,保持更新订单行项目方面的一致性可能会有利。也许不会立即清楚为什么某些更新是通过订单完成,而其他更新是通过订单行项目完成。此外,如果订单行具有20个以上的属性,则这可能表明这些属性之间存在分组的潜力,从而导致订单行上的属性减少。总体而言,只要确保操作是原子的、一致的,并与普遍语言相对应,那么方法2或4都可以。

1
谢谢你的回答!我同意缺乏一致性,但我认为这是我最好的选择。实际上,我们最初将许多订单行属性放在订单中,但随着我们继续分析领域,我们发现订单行更适合。然而,可能我们有点过度了... - Cork

6

有第五种方法可以实现这个功能。您可以触发一个领域事件,例如QuantityUpdatedEvent(order, product, amount)。让聚合内部处理它,通过遍历订单行的列表,选择匹配产品的订单行并更新其数量(或委托操作给更好的OrderLine)。


1
当一个实体有许多不同的修改方式时,我认为领域事件通常是正确的选择。Jimmy Bogard也写了一些不错的文章这里这里 - Justin J Stark
1
@Jeroen,我可能没有完全理解你的回答,或者你对域事件的概念有误解。事件是发生在过去的事情(修改状态)。我的理解是,在任何更改之前分派此事件,然后让处理程序对“订单”或“订单行”进行操作...您能澄清一下您的意图吗? - Wolfgang

5

域事件是最强大的解决方案。

但是如果这太过于复杂,您也可以使用参数对象模式对#2进行变体 - 在实体根上有一个单独的ModifyOrderItem函数。提交一个新的、更新后的订单项,然后订单在内部验证此新对象并进行更新。

因此,您的典型工作流将变成类似以下内容:

var orderItemToModify = order.GetOrderItem(id);
orderItemToModify.Quantity = newQuant;

var result = order.ModifyOrderItem(orderItemToModfiy);
if(result == SUCCESS)
{
  //good
 }
else
{
   var reason = result.Message; etc
}

这里的主要缺点是它允许程序员修改项目,但不能提交并意识到这一点。然而,它很容易扩展和测试。

2
这种情况在《快速领域驱动设计》中有所涉及。其中指出:“根实体可以将内部对象的瞬态引用传递给外部对象,条件是外部对象在操作完成后不再持有该引用。一种简单的方法是将值对象的副本传递给外部对象。” 我理解为将DTO传递给外部对象进行修改,并接受该DTO以应用修改。或者,您也可以在根实体上拥有修改子实体的方法,例如“order.ChangeItemQuantity(id, quantity)”。 - Douglas Gaskell

2

如果你的项目很小,想避免领域事件的复杂性,这里有另一个选择。创建一个处理订单规则的服务,并将其传递到OrderLine方法中:

public void UpdateQuantity(int quantity, IOrderValidator orderValidator)
{
    if(orderValidator.CanUpdateQuantity(this, quantity))
        Quantity = quantity;
}

CanUpdateQuantity需要将当前的OrderLine和新的数量作为参数。它应该查找订单,并确定更新是否会导致总订单数量的违规。(您需要确定如何处理更新违规情况。)
如果您的项目很小,不需要域事件的复杂性,这可能是一个不错的解决方案。
这种技术的缺点是您将Order的验证服务传递到了OrderLine中,而它实际上并不属于那里。相比之下,引发域事件可以将Order逻辑移出OrderLine。然后,OrderLine只需向世界说,“嘿,我正在更改我的数量。”,而Order验证逻辑可以在处理程序中进行。

0

使用DTO怎么样?

public class OrderLineDto
{
    public int Quantity { get; set; }
    public string Description { get; set; }
    public int ProductId { get; set; }
}

public class Order
{
    public int? Id { get; private set; }
    public IList<OrderLine> OrderLines { get; private set; }

    public void UpdateOrderLine(int id, OrderLineDto values)
    {
        var orderLine = OrderLines
            .Where(x => x.Id == id)
            .FirstOrDefault();

        if (orderLine == null)
        {
            throw new InvalidOperationException("OrderLine not found.");
        }

        // Some domain validation here
        // throw new InvalidOperationException("OrderLine updation is not valid.");

        orderLine.Quantity = values.Quantity;
        orderLine.Description = values.Description;
        orderLine.ProductId = values.ProductId;
    }  
}
  • 这里唯一的问题是OrderLines属性有公共getter,而且这个类的用户可以向集合中添加项目。我能想到的唯一方法是隐藏getter并添加新的getter,如果需要的话,它将返回DTO的集合。
  • UpdateOrderLine方法的id参数可以成为DTO的一部分,这样可能会更好。
  • 如果您想在传递给Order之前使用一些OrderLine验证,那么直接将OrderLine作为参数接受可能更好,而不是OrderLineDto

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