DDD战术设计-聚合

摘自
https://www.zhihu.com/column/p/381540329

设计业务实例

办公用品采购系统。

  1. 企业的员工可以通过该系统提交一个采购请求,一个请求包含了若干数量、若干类型的办公用品(称为采购项)。
  2. 主管负责对采购申请进行审批。
  3. 审批通过后,系统会根据提供商不同,生成若干订单。

如果

  1. 采用以数据库为中心的建模方式
    第一,对于模型的讨论过早地进入了实现领域,和业务概念脱开了联系,不便于持续地和业务人员协作;
    第二,技术细节和业务规则的细节纠缠在一起,很容易顾此失彼

  2. 面向对象
    业务规则如何保证,在传统的面向对象方法中并没有严格的实现约束

针对类似“修改采购项也是修改采购请求”的需求,DDD的做法

image.png

采购请求采购项组织到一起,看做一个更大的整体,称为“聚合”。
这个聚合内部的业务逻辑[例如“采购申请审核通过后,不得对采购申请条目进行更改”]应內建于聚合内部。
为了实现这一目标,我们约定:对采购项(实体)的一切操作(增加、删除、修改等),都是对采购请求(聚合根)的操作。

聚合定义

将实体和值对象划分为聚合并围绕着聚合定义边界。选择一个实体作为每个聚合的根,并仅允许外部对象持有对聚合根的引用。作为一个整体来定义聚合的属性和不变量,并把其执行责任赋予聚合根或指定的框架机制。
https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf

聚合本质

  • 建立一个比对象粒度更大的边界
  • 聚集紧密关联的对象 ---> 形成了一个业务上的对象整体。
  • 使用聚合根作为对外的交互入口 --> 保证多个互相关联的对象的一致性。

合理使用聚合,可以更容易地保证业务规则的一致性,减少了对象之间可能的耦合,提升设计的可理解性,降低出问题的可能性。


聚合划分的几个启发式规则

  • 生命周期一致性
  • 问题域一致性
  • 场景频率一致性
  • 尽量小的聚合

生命周期一致性

如果聚合根消失,聚合内的其他元素都应该同时消失。


如果采购请求(聚合根)不存在了,那么采购项当然也就失去了存在的意义。而商品、作为申请人的用户等对象,和采购请求之间则不存在此关系。

商品、作为申请人的用户等对象不纳入采购请求

问题域一致性

聚合表示的模型一定会位于同一个限界上下文之内


一个在线论坛,用户可以对论坛上用户的文章发表评论文章显然应该是一个聚合根。如果文章被删除,那么,用户的评论看起来也要同时消失。那么评论是否可以属于文章这个聚合?
一个图书网站,用户可以对图书发表评论,那么评论是否可以属于图书这个聚合?

评论这一个概念,在本质上和文章/图书这个概念相去甚远,因此不属于同一个问题域的对象
则不应该出现在同一个聚合中

image.png

需要依赖”最终一致性“来实现聚合之间的一致性。例如,在文章删除的时候,发送一个文章删除的消息。评论系统接收到文章删除消息之后,删除文章对应的评论。

场景频率一致性

场景(scenario)

  • 是业务用例的具体化描述
  • 反映了用户使用系统达成业务目标的方式。

经常被同时操作的对象,它们往往属于同一个聚合。而那些极少被同时关注的对象,一般不应该划为一个聚合。


考虑软件开发中的产品版本以及功能的关系。产品版本算不算是同一个问题域?

更合理的聚合

操作场景不一致的对象,或者说如果一个对象在不同场景下都会被使用,应该考虑把它们分到不同的聚合中。

尽量小的聚合

凡是不破坏以上三个一致性的情况,都没有必要把它们放到同一个聚合中

聚合之间如何关联?

采购申请及其属性(如状态、提交时间等)以及采购项属于一个聚合。但是,商品用户(提交人,审批人...)这些不能属于采购申请这个聚合。聚合之间如何关联?

引入ID对象来解决这个问题


image.png

引入ID对象带来的问题

  1. 某些场景下需要对信息进行第二次查询,而且无法利用 ORM 的 EagerFetch/LazyFetch 加载机制的遍历
    这不是损失,这类问题应该由外部服务,例如应用层服务来完成。
    好处,断开聚合,加快查询速度
  2. 为了断开聚合而额外引入的 Id 值对象,还能算是领域模型或者是 “统一语言” 的一部分吗?
    这是 DDD 的实现机制的一部分,它属于领域模型,但是请把其可见性控制在开发团队。没有必要和业务人员沟通这些概念
  3. 注意
    这个 Id 对象引用的只能是其他聚合根的 Id
    聚合根的 ID 应该做到全局唯一
    聚合内部的实体对象/值对象,保证内部的 ID 唯一即可。

代码实现方面的考虑

资源库(Repository)、工厂(Factory)面向聚合定义

  • 使用工厂来构造聚合对象是一种更好的对复杂性的封装
    在聚合以外,只应该有一个工厂对外可见,那就是聚合的工厂
  • 资源库是聚合的仓储机制
    一个聚合只能有一个资源库对象,那就是以聚合根命名的资源库。除此之外的其他对象,都不应该提供资源库对象。

代码结构与聚合保持一致

限界上下文,模块
└━聚合
       └━实体(包含聚合根)对象、值对象、资源库、工厂

聚合不可跨越部署的边界

  1. 如果系统采用了微服务架构,应该保持部署边界和限界上下文边界的一致
    不要让部署的粒度大于限界上下文的粒度,这样可以带来更好的业务灵活性和可伸缩性
  2. 从服务的最小边界上,不可让最小边界小于聚合的粒度,否则会带来大量的数据的一致性问题
    因为微服务之间的一致性一般需要通过最终一致性来保证,如果聚合跨越了部署边界将会是一致性的灾难

聚合改进了系统性能和可伸缩性

使用小的聚合
每个涉及访问的对象(事实上就是聚合)不可能很大,而所需的数据又恰如其分的都在,数据完整性和业务完整性就有了保障,还可以方便地进行水平扩展,性能和可伸缩性也就同时得到了满足。

你可能感兴趣的:(DDD战术设计-聚合)