DDD:项目计数是否属于领域模型?

DDD: Do item counts belong in domain model?

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

Category {
    int id;
    string name;
}

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

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

现在,我们的视图想要为每个类别投影 post 的计数。我的域不关心计数。我想我有两个选择:

  1. 在读取模型端监听 PostCreated 并使用类似 CategoryQueryHandler.incrimentCount(categoryId).
  2. 的方式增加计数
  3. 在域端侦听 PostCreated 并使用 CategoryRepo.incrimentCount(categoryId).
  4. 之类的方式增加计数

同样的问题适用于所有其他计数,例如用户的 post 数量、post 中的评论数量等。如果我不在任何地方使用这些计数,除了我的views 我应该只让我的查询处理程序处理持久化它们吗?

最后,如果我的一个域服务想要在类别中计数 post,我是否必须将计数 属性 实现到类别域模型中,或者该服务可以只需使用读取模型查询来获取该计数,或者使用存储库查询,例如 CategoryRepo.getPostCount(categoryId).

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

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

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

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

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

My domain doesn't care about count.

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

按照您的建议,将其作为 Post 创建的事件的计数,或者通过 运行 对 Post 商店的查询,或者......任何适合您的方法.

If I don't use these counts anywhere except my views should I just have my query handlers take care of persisting them?

那个,或者读取模型中的任何其他东西——但如果你的读取模型支持类似 select categoryId, count(*) from posts...

的东西,你甚至不需要那么多

domain services will ever want to have a count of posts in category

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

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

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)不是有效状态,那么它的有效性依赖于自身边界之外的数据,这意味着领域模型是错误的。

在更复杂的情况下,两个事务可以运行并发,这意味着我们可能会看到 aggregate(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 的示例。除非集合的表示位于聚合边界内,否则领域模型会遇到麻烦。但是关系数据库往往擅长集合 - 如果您的记录系统恰好是关系存储,您可以通过使用数据库 constraints/triggers.

来维护集合的完整性

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