聚合内部的实体能否在聚合外部访问或可见?

Can entities inside the aggregate be accessible or visible externally to the aggregate?

我是 DDD 的新手,我的问题对你们中的许多人来说似乎微不足道。

考虑学生和课程的情况。

只有当学生的年龄超过注册该课程所需的最低年龄时,学生才能注册该课程。

在我看来,Student 和 Course 可以被视为聚合,其中 Student 是根实体,Course 是子实体,age 是不变量。

学生应该有一个方法 Student.SubscribeTo(课程 course)并且该方法应该强制执行不变量 Student.Age >= Course.MinAge,否则会生成异常。

这在 DDD 方法中是否正确?或者我应该只传递给 SubscribeTo CourseId 吗? Student.SubscribeTo(int CourseId)

从我的角度来看,如果没有办法打破不变量,则应该允许从聚合外部访问 Course。如果我在我的代码的其他一些地方更改 Course.MinAge 我不会破坏我的业务需求,因为我希望只有在订阅课程时才尊重年龄,我不介意以后 Course.MinAge 变化。

如果业务需求状态不同,则情况不同:当 Course.MinAge 发生变化时,如果 Student.Age < Course.MinAge.

,则应将已注册课程的学生从课程中删除

我认为您的汇总不正确。 Course 实体可以独立存在,它不是 Student 实体的子实体。课程有自己的生命周期:例如如果学生离开学校,课程将继续存在。课程 ID 不依赖于学生 ID。学生可以保留课程ID,但它们是不同的集合。

无论如何,如果它们是一个聚合,对于仅将课程 id 传递给 "student.subscribeTo" 方法的问题,答案是否定的,您不能将子实体的 id 传递给聚合操作,因为子实体没有聚合外部已知的全局标识。他们在聚合中有本地 ID。

更新:

由于 Course 和 Student 是两个集合,规则 "student's age must be above the minimum age required to enroll the course" 不是不变的。为什么?因为不变量是关于聚合状态的业务规则,所以它必须始终在事务上保持一致。聚合定义事务一致性边界。

因此,该规则只是学生订阅课程时必须检查的验证规则("student.subscribeTo" 方法)。由于聚合不应该使用存储库,您可以将域服务传递给该方法,并且学生聚合将双重调度到域服务以便从课程 ID 获取课程。

查看 Vaughn Vernon 的红皮书 IDDD 的聚合章节(第 361-363 页)或同一作者的文章:

http://www.informit.com/articles/article.aspx?p=2020371&seqNum=4

希望对您有所帮助。

我也在学习 DDD,几天前我问了一个类似的问题,你可以找到 here

据我所知,唯一真正的答案是:这取决于。没有正确或错误的方法 本身 ,一切都必须为业务问题及其解决方案服务。虽然,有一些非常有用的指导方针和经验法则,例如:

  • 不要模拟现实。域模型是一种抽象,旨在解决特定问题。
  • 不要基于数据关系建模。每个关联都必须存在以执行规则或不变量,而不是因为那些 object 在现实生活中是相关的。开始基于行为建模。
  • 喜欢小聚合。
  • 更喜欢同时修改单个聚合(用例/事务),并使用最终一致性来更新其他聚合。
  • 仅按身份在聚合之间建立关联。

我觉得你这个场景的问题是少了很多。谁 拥有该协会 ,为什么?是否还有其他跨越学生和课程的用例?为什么用 student.SubscribeTo(course) 而不是 course.enroll(student)?请记住,DDD 的目标是解决复杂的领域逻辑,因此在接近模型的 写入端 时它会发光, 读取端 可以完成以许多不同的方式,而无需将大量关联放入模型中。

对于你所说的,仅仅验证年龄可能不是不变的,需要做一个大的聚合:

If I change the Course.MinAge in some other places of my code I don't break my business requirements, as I want the age be respected only when subscribing the course and I don't mind if later the Course.MinAge changes.

那么就没有理由强制 StudentCourse 始终保持一致(在这个特定的 context/scenario 中),也没有必要让它们成为同一个聚合体。如果您需要执行的唯一规则是 Student.Age >= Course.MinAge,您可以坚持使用简单的规则:

Student.SubscribeTo(Course course)

其中 StudentCourse 不是同一聚合的一部分。没有什么反对加载两个不同的聚合并在同一个事务中使用它们,只要您只修改一个。 (好吧,也没有什么反对在同一个事务中修改两个聚合,这只是一个经验法则,但在这种情况下你可能不需要打破它)。

这里 Student.SubscribeTo 将执行有关年龄的规则。我不得不说,让 Student 验证自己的年龄听起来 "weird",但也许这在您的模型中恰到好处(记住,不要模拟现实,模拟解决方案)。因此,Student 将有一个新的状态来保存课程的标识,而 Course 将保持不变。

Different case if business requirements state: when Course.MinAge changes students already enrolled in the course should be removed from the course if Student.Age < Course.MinAge.

在这里你必须首先(在领域专家的帮助下)回答一些问题:为什么要删除它们?他们应该立即被删除吗?如果他们在那一刻参加 class 怎么办?学生被除名是什么意思?

很有可能域中没有真正的需要在更改 MinAge 的同时删除学生(因为只有当所有操作都发生时才认为操作成功,如果不是,则什么也不会发生)。学生可能需要进入一个最终可以解决的新状态。如果是这种情况,那么您也不需要将它们作为同一聚合的一部分。

回答标题中的问题,不出所料:这取决于。拥有聚合的全部原因是为了保护一组以某种方式相关的实体的不变量。聚合不是 HAS-A 关系(不一定)。如果你必须保护一个跨越多个实体的不变量,你可以将它们做成一个聚合,并选择一个实体作为聚合根;该根是修改聚合的唯一访问点,因此始终强制执行每个不变量。允许直接引用聚合内的实体会破坏这种保护:现在您可以在根不知道的情况下修改该实体。由于无法从外部访问聚合内的实体,因此这些实体仅具有本地身份,并且在不引用根的情况下作为独立 objects 没有任何意义。不过,可以向根询问聚合内的实体。

可以 ,有时,将对内部实体的引用传递给另一个聚合,只要它是一个瞬态引用,并且没有人在聚合根之外修改该引用。然而,这会混淆模型并且边界开始变得模糊。更好的方法是传递该实体的副本,或者更好的方法是传递该实体的不可变视图(可能是值 object),这样就无法打破不变量。如果您认为传递对内部实体的引用不会破坏不变量,那么也许没有理由从聚合开始。