您的类别处于分层结构中。您通过添加包含子类别 ID 的 Children 属性对它们进行建模有什么特别的原因吗?
如果您通过删除 Children 属性并添加 ParentID 属性将参考方向从子级转向父级,这将解决您的一致性边界问题。添加新的类别不会影响父级。
您可以将方法 GetChildren(parentID) 或 GetChildrenIDs(parentID) 添加到 CategoryRepository 以获取子项或其 ID 类别(如果需要)。
编辑:
获得有关应用程序及其要求的更多信息在实施中很重要。不同的需求会导致不同的不变量,也会导致聚合的一致性边界不同。
我将针对特定要求给出一个示例实现。它们并不完整,因为编写所有案例的所有代码将需要大量文本。
让我们问几个关于类别排序的问题。
问题 1:ParentSortIndex 是如何从 Command 发送方计算出来的,以便设置为 命令?
问题 2: 如果一个 Category 没有子级,那么接收带有 的 Command 是否有效ParentSortIndex = 10?
问题 3: 值 ParentSortIndex 重要还是类别的顺序唯一重要?
假设 Categories 的顺序是唯一重要的事情,它是如何实现的,或者 SortIndex 的值并不重要。
首先让我们介绍一下SortingIndex的概念。现在,让我们考虑一下这个概念的实现。我们可以使用 float 作为 SortingIndex 的值而不是 int(如果我们期望有很多 Categories,则可以使用 double)。花车有一个很好的属性,你可以(几乎)总能找到一个适合其他两个花车的。例如,如果您有 1 和 2,则 1.5 在它们之间,1.2 在 1 和 1.5 之间等等。
接下来让我们添加 CategoryRepository.GetSortingIndicesForChilren(parentId) 方法。此方法将为父级的所有子级获取具有 CategoryGuid 和 SortingIndex 属性的对象,以便我们可以计算紧邻请求的 的 SortingIndex类别。
这将避免加载所有子项。从 Repositories 返回特殊值是一种不错的技术。在DDD book 中,Eric Evans 对此进行了解释,并表示 Repositories 返回此类包含一些信息或数据的特殊对象是很正常的。
接下来让我们指定要将新子类别放置到哪个子类别,而不是指定具体的索引值。 (我们可能希望将它放在类别的上方,但为了简单起见,我将跳过这种情况。它可以通过添加到 Command 的 enum { 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);
}
}
在上述情况下,由于没有任何规则可以为索引指定特定值,因此我们可以通过避免子节点之间的冲突和必须改变任何状态的方式来实现它们。
拥有一个包含孩子的集合会导致该集合的状态突变。
使用整数会导致索引之间发生冲突的可能性很高,并且会导致重新计算子索引。这将跨越多个聚合。
添加新的Category很简单,因为我们只需要找到在指定类别之后(或两个类别之间)的索引,而不需要修改集合或其他子类别。 p>
如果上述情况不成立,并且 SortingIndex 的值有规则,则意味着需要满足额外的不变量,并且会导致不同的一致性边界。
您仍然可以通过具有最终一致性或使用Saga 来实现这一点,该Saga 将管理父类别和新类别之间的分布式事务。在这种情况下,您无法逃避最终一致性,并且会担心其他事情。
如果您认为最终一致性是一个问题并且您不想处理复杂性,那么如果您的应用程序允许,您可以在同一个事务中修改两个聚合。您不能在分布式应用程序中执行此操作。