【问题标题】:DDD: Do item counts belong in domain model?DDD:项目计数是否属于域模型?
【发布时间】:2016-03-01 06:41:57
【问题描述】:

假设您正在为论坛建模,并且您正在尽最大努力利用 DDD 和 CQRS(只是单独的读取模型部分)。你有:

Category {
    int id;
    string name;
}

Post {
    int id;
    int categoryId;
    string content;
}

每次创建新帖子时都会引发域事件PostCreated

现在,我们的视图想要预测每个类别的帖子数。我的域不关心计数。我想我有两个选择:

  1. 在读取模型端侦听 PostCreated 并使用类似 CategoryQueryHandler.incrimentCount(categoryId) 的内容增加计数。
  2. 在域端侦听PostCreated 并使用CategoryRepo.incrimentCount(categoryId) 之类的内容增加计数。

同样的问题适用于所有其他计数,例如用户发布的帖子数量、帖子中的 cmets 数量等。如果除了我的视图之外我不在任何地方使用这些计数,我是否应该让我的查询处理程序来处理坚持他们?

最后,如果我的某个域服务想要计算类别中的帖子计数,我是否必须在类别域模型上实现计数属性,或者该服务是否可以简单地使用读取模型查询来获取该计数,或者存储库查询,例如 CategoryRepo.getPostCount(categoryId)

【问题讨论】:

    标签: repository domain-driven-design cqrs


    【解决方案1】:

    我的域不关心计数。

    这相当于说您没有任何需要或管理计数的不变量。这意味着不存在计数有意义的聚合,因此计数不应该在您的域模型中。

    按照您的建议将其作为 PostCreated 事件的计数来实现,或者通过对 Post 存储运行查询来实现,或者......任何适合您的方式。

    如果除了我的视图之外我不在任何地方使用这些计数,我是否应该让我的查询处理程序负责持久化它们?

    那个,或者读取模型中的其他任何东西——但是如果你的读取模型支持像select categoryId, count(*) from posts...这样的东西,你甚至不需要那么多。

    域服务将永远希望对类别中的帖子进行计数

    对于一个域服务来说,这是一件很奇怪的事情。域服务通常是无状态查询支持 - 通常它们被聚合用于在命令处理期间回答某些问题。它们自己实际上并不强制执行任何业务不变量,它们只是在这样做时支持聚合。

    在两个级别上查询写入模型要使用的计数的读取模型没有意义。首先,读取模型中的数据是陈旧的——您从该查询中获得的任何答案都可能在您完成查询和尝试提交当前事务之间发生变化。其次,一旦您确定陈旧数据有用,就没有特别的理由更喜欢在事务期间观察到的陈旧数据而不是之前的陈旧数据。也就是说,如果数据无论如何都是陈旧的,您不妨将其作为命令参数传递给聚合,而不是将其隐藏在域服务中。

    OTOH,如果您的域需要它——如果存在一些约束计数的业务不变量,或者使用该计数来约束其他事物的业务不变量——那么需要在一些聚合中捕获该不变量> 控制计数状态。

    编辑

    考虑两个同时运行的事务。在事务 A 中,聚合 id:1 运行需要对象计数的命令,但聚合不控制该计数。在事务 B 中,正在创建聚合 id:2,这会更改计数。

    简单的例子,两个交易碰巧发生在连续的区块中

    A: beginTransaction
    A: aggregate(id:1).validate(repository.readCount())
    A: repository.save(aggregate(id:1))
    A: commit
    // aggregate(id:1) is currently valid
    
    B: beginTransaction
    B: aggregate(id:2) = aggregate.new
    B: repository.save(aggregate(id:2))
    B: commit
    // Is aggregate(id:1) still in a valid state?
    

    我表示,如果 aggregate(id:1) 仍处于有效状态,则其有效性不依赖于 repository.readCount() 的及时性——使用事务开始之前的计数本来也一样好。

    如果aggregate(id:1)不是有效状态,那么它的有效性依赖于它自己边界之外的数据,这意味着领域模型是错误的。

    在更复杂的情况下,两个事务可以同时运行,这意味着我们可能会看到聚合(id:2)的保存发生在计数的读取和聚合(id:1)的保存之间,像这样

    A: beginTransaction
    A: aggregate(id:1).validate(repository.readCount())
    // aggregate(id:1) is valid
    
    B: beginTransaction
    B: aggregate(id:2) = aggregate.new
    B: repository.save(aggregate(id:2))
    B: commit
    
    A: repository.save(aggregate(id:1))
    A: commit
    

    考虑一下为什么有一个控制状态的聚合可以解决问题可能会很有用。让我们改变这个例子,这样我们就有一个包含两个实体的聚合......

    A: beginTransaction
    A: aggregate(version:0).entity(id:1).validate(aggregate(version:0).readCount())
    // entity(id:1) is valid
    
    B: beginTransaction
    B: entity(id:2) = entity.new
    B: aggregate(version:0).add(entity(id:2))
    B: repository.save(aggregate(version:0))
    B: commit
    
    A: repository.save(aggregate(version:0))
    A: commit
    // throws VersionConflictException
    

    编辑

    提交(或保存,如果您愿意)可以抛出的概念是一个重要的概念。它强调模型是一个独立于记录系统的实体。在简单的情况下,模型防止无效写入,记录系统防止写入冲突。

    务实的答案可能是让这种区别变得模糊。尝试对计数应用约束是 Set Validation 的一个示例。除非集合的表示位于聚合边界内,否则域模型将遇到问题。但是关系数据库往往擅长集合——如果你的记录系统恰好是一个关系存储,你可以通过使用数据库约束/触发器来维护集合的完整性。

    您如何处理此类问题应基于对特定故障的业务影响的理解。缓解,而不是预防,可能更合适。

    【讨论】:

    • 不能使用 count 来约束某些东西的不变量,使用 CategoryRepo.getPostCount(categoryId) 在没有任何聚合控制计数状态的情况下获取该计数吗?因为从技术上讲,仅通过计算帖子的数量,计数信息就已经存在。我的推理基于 Eric Evans 在他的书中关于存储库的说法:“尽管大多数查询返回一个对象或对象集合,但它也符合返回某些类型的汇总计算的概念,例如对象计数,或模型打算计算的数字属性的总和。”
    • @DenisPshenov Eric Evans 在 CQRS 不存在的时候写了这篇文章。现在你会把getPostCount()放在一个读取模型Facade上,而不是一个命令端存储库。
    • 是的,我知道您会放入一个读取模型以供您的视图使用。但是,如果域也需要该计数,为什么我们不能将它也放入存储库中。以这种方式看待它 - 如果某个不变量想知道我们系统中的类别总数怎么办。您会创建另一个控制类别计数的聚合吗?或者您只是要求您的存储库告诉您它有多少个类别?
    • @DenisPshenov 如果域需要计数,它将使用相同的读取模型。 CQRS 意味着您的模型是专业的,而不是一个属于该领域而另一个不属于。
    • @DenisPshenov 读取模型是读取模型。命令用例可以使用一个概念的读取模型来获取另一个概念的命令模型所需的数据。我们将事物分开的原因是为了在我们只想阅读内容的情况下使用更简单的模型。您的应用程序中没有大的命令部分和另一个单独的读取部分。 C/Q 适用于最细粒度的级别,即表示 one 概念的模型。
    【解决方案2】:

    当涉及到事物的计数时,我认为必须考虑您是否真的需要将计数保存到数据库中。

    在我看来,在大多数情况下,您不需要保存计数,除非它们的计算非常昂贵。所以我不会有CategoryQueryHandler.incrementCountCategoryRepo.incrementCount

    我只需要一个 PostService.getPostCount(categoryId) 来运行类似

    的查询
    SELECT COUNT(*) 
    FROM Post 
    WHERE CategoryId=categoryId
    

    然后在您的 PostCreated 事件触发时调用它。

    【讨论】:

    • 对于读取端,出于性能原因,最好已经存在这些计数。对于域,可以使用类似于您所描述的内容,因为在我的系统中写入很少。
    猜你喜欢
    • 2023-03-27
    • 2018-12-14
    • 1970-01-01
    • 2012-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-05
    • 1970-01-01
    相关资源
    最近更新 更多