关于红皮书聚合示例的问题

Question about aggregate examples from red book

在红皮书(实施领域驱动设计)中,Vernon Vaughn 展示了 Scrum 核心领域的聚合示例。不幸的是,代码示例只是片段。

第二次尝试将模型拆分为多个聚合,而不是使用单个大聚合。因此,planBacklogItem 方法的方法契约从

更改
public class Product ... {
    ...
    public void planBacklogItem(
        String aSummary, String aCategory,
        BacklogItemType aType, StoryPoints aStoryPoints) {
            ...
    }
    ...
}

public class Product ... {
    ...
    public BacklogItem planBacklogItem(
        String aSummary, String aCategory,
        BacklogItemType aType, StoryPoints aStoryPoints) {
        ...
    }
}

应用服务看起来像这样:

public class ProductBacklogItemService ... {
    ...
    @Transactional
    public void planProductBacklogItem(
        String aTenantId, String aProductId,
        String aSummary, String aCategory,
        String aBacklogItemType, String aStoryPoints) {

      Product product =
            productRepository.productOfId(
                    new TenantId(aTenantId),
                    new ProductId(aProductId));

      BacklogItem plannedBacklogItem =
            product.planBacklogItem(
                    aSummary,
                    aCategory,
                    BacklogItemType.valueOf(aBacklogItemType),
                    StoryPoints.valueOf(aStoryPoints));

      backlogItemRepository.add(plannedBacklogItem);
    }
    ...
}

我理解聚合的概念和示例代码,但我想知道为什么 planBacklogItem 在多聚合版本中没有声明为静态。该方法不访问任何实例数据。使用单独的聚合来实现更好的并发访问。因此,我不明白为什么应用程序服务首先从存储库中提取完整的产品,而需要 none 的数据。

由于 BacklogItem 使用 ID,因此无需从存储库读取产品即可创建它。如果产品聚合有大量数据并且子聚合被频繁访问,实施可能会导致性能问题。

我能想到的唯一解释是它应该确保产品的存在。但是为什么不使用 productRepository.existsProduct(productId) 之类的方法呢?

我正在使用 C#,不了解@Transactional 的所有魔力。 Vaughn 没有说明隔离级别。读取产品和写入 BacklogItem 之间的竞争条件可能会发生。 @Transactional 是否创建可序列化的事务?我对此表示怀疑,因为并非所有存储都支持它。如果事务的隔离级别未序列化或产品在读取期间未锁定,则可以在写入 BackLogItem 之前将其删除。 (这可能不是 Scrum 示例的业务案例,但它是一个普遍关注的问题)。

恐怕我错过了一些重要的事情。感谢您的帮助。

The method does not access any instance data.

不确定书中的示例是否不正确,但它确实使用了 GitHub samples 上的实例数据。

public BacklogItem planBacklogItem(
            BacklogItemId aNewBacklogItemId,
            String aSummary,
            String aCategory,
            BacklogItemType aType,
            StoryPoints aStoryPoints) {

        BacklogItem backlogItem =
            new BacklogItem(
                    this.tenantId(), // <--
                    this.productId(), // <--
                    aNewBacklogItemId,

If the isolation level of the transaction is not serialized or the product is not locked during read it could be deleted before the BackLogItem was written.

的确,可能存在竞争条件(假设我们可以删除产品)。 TBH 我对 LevelDB 了解不够,也没有深入研究实际的实现细节,但是传统的外键约束可以防止关系数据库中出现孤儿。

但是,这对于逻辑删除(例如归档)并不适用,所以在这些情况下,我想有这些选择:

  1. 忽略这个问题。也许在产品归档后一毫秒计划积压项目实际上并不重要? Race Conditions Don't exist?

  2. 锁定 Product AR。这可以通过多种方式完成,例如强制乐观锁定版本升级。

  3. 应用最终一致的补偿措施,例如取消计划的计划积压项目。