选择正确的聚合根的策略,因此事务不会跨多个聚合

时间:2019-04-27 21:30:37

标签: transactions domain-driven-design aggregate

我正在对Category / Menu域上下文进行建模,并决定为此上下文具有2个聚合根。

 public class MenuItem : Aggregate<Guid>
{
    public List<string> ImageUrls { get; set; }
    public decimal Price { get; set; }
    public IList<ExtraProperty> Extras { get; set; }
    public ITranslationList<MenuItemTranslation> Translations { get; set; }
    public bool Active { get; set; }
}




 public class Category : Aggregate<Guid>
 {
    public ITranslationList<CategoryTranslation> Translations { get; set;}
     public SortedList<int,Guid> Children { get; set; }
    public List<string> ImageUrls { get; set; }

    internal Category() { }


}

在类别模型中,Children属性是子类别和MenuItems ID的排序列表。

现在假设我要创建类别。我有一个用于此目的的命令:

  public class CreateCategoryCommand:ICommand
  {
    public Guid Id { get; set; }
    public List<string> ImageUrls { get; set; }
    public ITranslationList<CategoryTranslation> Translations  { get; set; }
    public Guid UserId { get; set; }
    public Guid? ParentId { get; set; }
    public int ParentSortIndex { get; set; }
  }

所以这里发生的是,我创建类别,如果设置了ParentId属性,则从存储库中获取具有该ID的类别,将记录添加到Children排序列表中,并保存父类别。

问题是在这种情况下,事务跨2个聚合(新创建的聚合和父聚合)。

因此,我感到我对聚合边界的建模不正确。一方面,我尝试按照Vaughn Vernon的建议使聚合尽可能小(这就是为什么Category包含Id引用,而不是实际对象的原因),另一方面,事务在保存一个聚合时会跨越多个聚合,这就是设计缺陷恕我直言。 / p>

您对此环境进行建模的策略/建议/意见是什么?

1 个答案:

答案 0 :(得分:1)

您的类别采用分层结构。通过添加包含子类别ID的 Children 属性对它们进行建模,是否有任何特殊原因?

如果您通过删除 Children 属性并添加 ParentID 属性将参考方向从孩子转向父母,这将解决您的一致性边界问题。添加新的类别不会影响父级。

您可以将方法 GetChildren(parentID) GetChildrenIDs(parentID)添加到 CategoryRepository ,以获取子项或它们的ID。 类别(如果需要)。

编辑:

拥有有关应用程序及其要求的更多信息在实现中很重要。不同的要求导致不同的不变性,并且会导致聚合的一致性边界不同。

我将给出一个针对特定需求的示例实现。它们并不完整,因为在所有情况下编写所有代码都需要大量文本。

让我们问几个有关类别排序的问题。

  • 问题1:如何通过 Command 发件人计算 ParentSortIndex ,以便可以将其设置为命令

  • 问题2:如果类别没有孩子,则收到带有命令是否有效? ParentSortIndex = 10?

  • 问题3: ParentSortIndex 是重要的还是类别的排序是唯一重要的事情?

让我们说 Categories 的顺序是唯一重要的事情,以及它的实现方式,或者 SortIndex 的值并不重要。

首先让我们介绍 SortingIndex 的概念。现在,让我们考虑一下此概念的实现。我们可以将float用作 SortingIndex 的值,而不是int(如果期望很多 Categories ,则可以使用double)。浮点数具有一个不错的属性,您可以(几乎)总是找到一个适合其他两个浮点数的属性。例如,如果您有1和2,则介于1.5和1.2之间,以此类推。

接下来,我们添加 CategoryRepository.GetSortingIndicesForChilren(parentId)方法。此方法将获得一个对象,该对象具有父对象所有子代的 CategoryGuid和SortingIndex 的属性,以便我们可以计算紧靠请求的 SortingIndex 。类别

这将避免必须加载所有子代。从存储库返回特殊值是一种很好的方法。埃里克·埃文斯(Eric Evans)在DDD book中对此进行了解释,并说存储库返回包含某些信息或数据的特殊对象是很正常的。

接下来,让我们指定要将新的子类别放置到哪个子类别,而不是指定具体的索引值。 (我们可能希望将其放在该类别的下面,但为简单起见,我将跳过这种情况。可以使用可以添加到 Command 的枚举{placeAbove,placeBellow}来解决此问题)

public class SortingIndex : ValueObject {

  public static readonly MinValue = new SotringIndex(float.MinValue);
  public static readonly MidValue = new SotringIndex(float.MaxValue);
  public static readonly MaxValue = new SotringIndex(float.MaxValue);

  public float Value { get; private set; }

  public SortingIndex(float value) { .... }

  public SortingIndex GetBtween(SortingIndex other) { ... }

  public static operator > (OrderingPriority other) { .. }
  public static operator >= (OrderingPriority other) { .. }
  // other operators <=, ==, != etc.
}

public class Category : Aggregate<Guid> {
   public Guid ParentGuid { get; private set; }
   public SortingIndex SortingIndex { get; private set; }
   // constructor and other stuff......
}

public class CreateCategoryCommand : ICommand
{
    public Guid? ParentId { get; set; }
    public Guid? CategoryGuidToPlaceNextTo { get; set; }
    // other stuff...
}

public class CreateCategoryCommandHandler {

  public void Handle(CreateCategoryCommand cmd) {

    var sortingIndex = SortingIndex.MidValue;

    // start with mid value. If there aren't any children, this will be the 
       first. Later when we add other children we can calculate an index 
       before of after this one.

    if(cmd.ParentID != null && cmd.CategoryGuidToPlaceNextTo != null) {

          var childrenSortingIndices = CategoryRepository
                           .GetSortingIndicesForChilren(cmd.ParentID);

           sortingIndex = PlaceChildNextTo(
                            childrenSortingIndices,
                            cmd.CategoryGuidToPlaceNextTo);
     }

    var category = new Category(cmd.ID, cmd.ParentID, sortingIndex, ...);

    CategoryRepository.Save(category);
  }
}

在上述情况下,由于没有任何规则为索引指定特定的值,因此我们可以以避免子对象之间发生冲突并必须更改任何状态的方式来实现它们。

与孩子一起收藏将导致该收藏的状态突变。

具有整数会导致索引之间发生冲突的可能性很高,并将导致子索引的重新计算。这将跨越多个聚合。

添加新的类别很简单,因为我们只需要查找指定类别之后(或两个类别之间)的索引,而无需修改集合或其他子类别。

如果上述内容不正确,并且 SortingIndex 的值存在规定,则意味着还有其他不变量需要满足,并且会导致不同的一致性边界。

您仍然可以通过保持最终一致性或使用Saga来管理父类别和新类别之间的分布式事务来实现此目的。在这种情况下,您将无法避免最终的一致性,并且还需要担心其他事情。

如果您仍然认为最终的一致性是一个问题,并且不想处理复杂性,则可以在应用程序允许的情况下在同一事务中修改两个聚合。您无法在分发应用程序中执行此操作。