DDD 聚合的设计

时间:2024-03-14 18:49:36

聚合的设计

 

这实际上就是高质量设计聚合的基本过程:

第一步:弄清楚对象图的结构。可以以细化后的领域分析模型作为领域设计模型对象图,理清它们之间的关系,辨别类为实体还是值对象,并保证类关系的单一导航方向。

【例子】

从单一导航方向的视角对关系建模

若存在双向关联的类属于同一个聚合,由于聚合类的实体之间可以采用对象引用的形式,就应保留“主类型”导航向“从类型”的方向。

例如,聚合内的 Order 与 OrderItem 之间的关系,既可以描述为订单拥有多个订单项,也可以描述为订单项属于某个订单。订单为主,订单项为从,故而应该只保留从 Order 到 OrderItem 的单一导航方向。

 

若存在双向关联的类分属两个不同的聚合,且为各自的聚合根实体,为了降低彼此的依赖强度,往往是保留“从类型”导航向“主类型”的方向。例如,Customer 与 Order 是两个聚合的根实体,客户为主,订单为从,则应该保留从 Order 聚合根到 Customer 聚合根的单一导航方向。

 

 

第二步:以关系强弱为界,以聚合边界为刀,逐一分解。仅仅将具有继承关系与合成关系的类放入聚合边界内,其余类则一刀解开,各自为独立的聚合。

第三步:怵然为戒,谨慎设计聚合。针对聚合边界模糊的地方,运用聚合设计原则做进一步推导。

 

 

这个过程可戏称此为聚合设计的庖丁解牛过程,可进一步精简为:

理顺对象图

分解关系薄弱处 。在理顺对象图之后,就可以将继承、合成、聚合与协作视为判断关系强弱的标志,然后直接将关系耦合最高的继承或合成关系的类放入聚合边界中。

调整聚合边界

 

调整聚合边界

  • 调整聚合边界是一个细致活儿。凡是对聚合边界的划分存有疑惑之处,都应该遵循聚合设计原则作进一步推导。比如一个聚合的非聚合根实体不允许被聚合外部的对象直接引用等等。

 

  • 在通过概念完整性与独立性甄别了聚合边界之后,应从不变量着手,进一步夯实聚合的设计。不变量可以提炼自业务规则

【例子】如果两个领域在一个不变量的约束之内,那么他们不应该设计成两个聚合

一种简单地方法是将 Calendar 实体独立为一个聚合。然而分析需求,我们发现 Course 与 Calendar 之间存在着不变量的约束关系,例如同一个课程不能指定两个日期存在重叠的日程,两个相邻的日程必须间隔规定的天数。这个不变量需要通过 Course 聚合根来保障,防止被外部调用者破坏,因此需要将它们放在一个聚合中。

  • 最后,通过事务的边界再一次确认聚合的边界。若聚合内的实体与聚合根实体不具备数据强一致性,可以考虑移出聚合若出现跨聚合之间的数据一致性,确定是否需要合并

 

 

 

实体还是值对象?

除了要有效控制类之间的关系外,还需要分辨领域分析模型中的领域类究竟是实体还是值对象。在前面介绍值对象时,我写道:

 

在进行领域驱动设计时,我们应该优先考虑使用值对象来建模而不是实体对象。因为值对象没有唯一标识,于是我们卸下了管理身份标识的负担;因为值对象是不变的,所以它是线程安全的,不用考虑并发访问带来的问题。值对象比实体更容易维护,更容易测试,更容易优化,也更容易使用,因此在建模时,值对象才是我们的第一选择。

在设计聚合时,值对象更容易被管理,当然也更容易识别其归属。这是因为聚合只能以实体为根,

 

 

 

设计案例

https://gitbook.cn/gitchat/column/5cdab7fb34b6ed1398fd8de7/topic/5cdb7c5e34b6ed1398fd93fa

 

领域设计图跟类图的差别

 

 

  • 使用不同的颜色标注实体和值对象
  • 如果是值对象为两个实体的属性,要画两个值对象
  • DDD 聚合的设计

 

  • 组合关系跟类图是一致的,引用关系要从单一导航方向的视角对关系建模。聚合内部的引导方向跟聚合之前的引导方向的约定不一样。

 

「案例」培训领域模型的聚合设计

DDD 聚合的设计

 

聚合 还是值对象?

如果两个领域在一个不变量的约束之内,那么他们不应该设计成两个聚合

一种简单地方法是将 Calendar 实体独立为一个聚合。然而分析需求,我们发现 Course 与 Calendar 之间存在着不变量的约束关系,例如同一个课程不能指定两个日期存在重叠的日程,两个相邻的日程必须间隔规定的天数。这个不变量需要通过 Course 聚合根来保障,防止被外部调用者破坏,因此需要将它们放在一个聚合中。

 

违反聚合外部的对象引用除根实体之外的内部对象约束解决的办法

通过身份标识进行聚合之间的协作。虽然协作原则要求“聚合外部的对象不能引用除根实体之外的任何内部对象”,但并没有限制对这些内部对象身份标识的引用,即 Training 不引用 Calendar 实体,转而引用它的身份标识 CalendarId。同时,Training 与 Course 聚合之间的协作也将通过 CourseId: