【问题标题】:Question about aggregate examples from red book关于红皮书聚合示例的问题
【发布时间】:2022-01-03 12:40:35
【问题描述】:

在红皮书(实施领域驱动设计)中,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 在多聚合版本中没有声明为静态。该方法不访问任何实例数据。使用单独的聚合来实现更好的并发访问。因此,我不明白为什么应用程序服务首先从存储库中提取一个完整的产品,而不需要它的任何数据。

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

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

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

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

【问题讨论】:

    标签: domain-driven-design aggregates


    【解决方案1】:

    该方法不访问任何实例数据。

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

    public BacklogItem planBacklogItem(
                BacklogItemId aNewBacklogItemId,
                String aSummary,
                String aCategory,
                BacklogItemType aType,
                StoryPoints aStoryPoints) {
    
            BacklogItem backlogItem =
                new BacklogItem(
                        this.tenantId(), // <--
                        this.productId(), // <--
                        aNewBacklogItemId,
    

    如果事务的隔离级别未序列化或产品在读取期间未锁定,则可以在写入 BackLogItem 之前将其删除。

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

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

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

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

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

    【讨论】:

    • 感谢您的回答。是的,这本书的代码和 GitHub 的代码不同,这更有意义。我仍然不明白为什么当有较小的聚合时我需要拉根对象。这真的有必要吗?
    • 在关系数据库使用外键约束的情况下,拉产品来计划新的 BacklogItem 对我来说更没有意义。概括来说,这是一个 1:n 问题。如果 1 面和 n 面之间没有不变量,我为什么要阅读 1 面以在 n 面添加项目?
    • @M.Koch 很可能是更紧密地遵循通用语言的问题。你用性能换取表现力,AR 上的工厂方法仍然有助于隐藏一些创建复杂性。此外,如果业务规则有新的不变量或更改,则编码方式的设计提供了更大的灵活性。
    • 感谢您的回答。你的解释也是我唯一能想到的。 Vaughn 没有解释这个选择或提到性能方面,这仍然让我感到困惑。在我看来,他认为一些我不知道或不理解的基本知识是理所当然的。我认为将 planBacklogItem 方法放在产品类中是合理的,但将其声明为静态方法会避免对产品聚合的不必要读取。如果有一个新的不变量,则无论如何都可能会放弃多个聚合。
    • 我直接从 Vaughn Vernon 那里得到了很好的回答。正如 plalx 所假设的,“product.planBacklogItem(...) 的主要目标是表达无处不在的语言”。 “是的,它可能是静态工厂方法或域服务”,“但验证仍然会往返于数据库,并且 Product 非常小”。 “在 product.planBacklogItem(...) 内部可以检查产品是否仍然打开(从不删除产品)”。
    猜你喜欢
    • 2017-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多