聚合
在 DDD 中,实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。
那聚合在其中起什么作用呢?
社会由个体组成,我们每个人都是其中一员。随着社会发展,社团、机构、部门等组织应运而生,我们从个体逐渐成为组织的一部分。在组织中,大家协同工作,朝着共同目标奋进,能发挥出更大的力量。
在领域模型里,实体和值对象类似于个体,而聚合则如同让实体和值对象协同工作的组织。聚合确保这些领域对象在实现共同业务逻辑时,数据保持一致。简单来说,聚合由业务和逻辑紧密关联的实体和值对象组合而成,是数据修改和持久化的基本单元。每个聚合对应一个仓储,用于实现数据持久化。
聚合有聚合根和上下文边界。这个边界依据业务单一职责和高内聚原则,界定了聚合内部包含的实体和值对象。而且,聚合之间的边界是松耦合的。按此方式设计的微服务,自然具备“高内聚、低耦合”的特性。
在DDD分层架构中,聚合属于领域层。领域层包含多个聚合,共同实现核心业务逻辑。聚合内的实体采用充血模型,实现个体业务能力以及业务逻辑的高内聚。
跨多个实体的业务逻辑通过领域服务实现,跨多个聚合的业务逻辑则通过应用服务实现。例如,若某个业务场景需要同一个聚合中的A和B两个实体共同完成,那么这段业务逻辑可用领域服务实现;若业务逻辑需要聚合C和聚合D中的两个服务共同完成,这时就可以用应用服务来组合这两个服务。
聚合根
聚合根的主要作用,是防止复杂数据模型因为缺乏统一业务规则的管控,而出现聚合、实体之间数据不一致的情况。在传统数据模型里,每个实体地位平等,若任由实体随意调用和修改数据,极有可能造成实体间数据逻辑的混乱。要是采用锁的方式来解决,又会增加软件复杂度,降低系统性能。
如果把聚合看作一个组织,那么聚合根就相当于这个组织的负责人,也被称为根实体。它既是实体,又承担着聚合管理者的角色。
从实体角度来看,聚合根具备实体的属性和业务行为,能够实现自身的业务逻辑。
作为聚合的管理者,聚合根在聚合内部发挥着协调作用,确保实体和值对象依照既定的业务规则,协同完成共同的业务逻辑。
在聚合之间,聚合根是聚合对外的接口。它通过聚合根 ID 关联的方式,接收外部任务和请求,并在上下文范围内实现聚合之间的业务协作。也就是说,聚合之间是通过聚合根 ID 进行关联引用的。外部对象若要访问其他聚合的实体,不能直接进行访问,而是要先访问聚合根,再通过聚合根导航到聚合内部的实体。
怎样设计聚合?
DDD 领域建模通常采用事件风暴,它通常采用用例分析、场景分析和用户旅程分析等方法,通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,并梳理领域对象之间的关系,找出聚合根,找出与聚合根业务紧密关联的实体和值对象,再将聚合根、实体和值对象组合,构建聚合。
下面我们以保险的投保业务场景为例,看一下聚合的构建过程主要都包括哪些步骤。
图片
在投保过程中构建聚合,可按以下步骤进行:
第一步:采用事件风暴梳理实体和值对象
基于业务行为,运用事件风暴方法,全面梳理在投保过程中涉及这些行为的所有实体和值对象。例如,常见的有投保单、标的、客户、被保人等。
第二步:确定聚合根
从众多实体中挑选出适合担任对象管理者的根实体,即聚合根。判断一个实体能否成为聚合根,可结合以下场景展开分析:该实体是否拥有独立的生命周期;是否具备全局唯一 ID;是否能够创建或修改其他对象;是否存在专门的模块对其进行管理。在相关图示中,投保单和客户实体就是聚合根。
第三步:构建聚合
依据业务单一职责和高内聚原则,找出与聚合根紧密关联、相互依赖的所有实体和值对象。由此构建出一个对象集合,这个集合包含唯一的聚合根以及多个实体和值对象,这便是聚合。在图中,我们构建出了客户和投保这两个聚合。
第四步:绘制对象引用和依赖模型
在聚合内部,根据聚合根、实体和值对象之间的依赖关系,绘制出对象的引用和依赖模型。需要特别说明的是,投保人和被保人的数据,是通过关联客户 ID 从客户聚合中获取的,在投保聚合里,它们属于投保单的值对象。这些值对象的数据是客户数据的冗余,即便未来客户聚合的数据有所变动,也不会对投保单的值对象数据产生影响。从图中还能清晰看到实体之间的引用关系,比如在投保聚合中,投保单聚合根引用了报价单实体,而报价单实体又引用了报价规则子实体。
第五步:划分限界上下文
将多个聚合依据业务语义和上下文,划分到同一个限界上下文内。
聚合的一些设计原则
我们不妨先看一下《实现领域驱动设计》一书中对聚合设计原则的描述,原文是有点不太好理解的,我来给你解释一下。
在一致性边界内建模真正的不变条件是 DDD 设计中的重要原则。聚合的意义在于封装真正的不变性,并非简单拼凑对象。每个聚合内部都有一套既定的业务规则,其中的实体和值对象遵循这些规则运转,以此保证对象数据的一致性。而在聚合边界之外的事物,与该聚合毫无关联,这也正是聚合能够达成业务高内聚的关键因素。
设计小聚合也十分关键。聚合规模若设计得过大,会因涵盖过多实体,使实体间管理难度大增。在高频操作场景下,易引发并发冲突或数据库锁问题,最终降低系统可用性。与之相对,小聚合设计能减少因业务扩张导致聚合重构的几率,让领域模型对业务变化的适应性更强。
聚合间的引用依靠关联外部聚合根 ID 来实现,而非直接的对象引用。若把外部聚合的对象纳入本聚合边界内管理,不仅会模糊聚合边界,还会提升聚合间的耦合程度,所以要通过唯一标识引用其它聚合。
聚合内部数据需保持强一致性,而聚合之间实现最终一致性即可。一次事务中,最多只能改变一个聚合的状态。要是业务操作涉及多个聚合状态变更,应借助领域事件异步修改相关聚合,从而实现聚合间的解耦(领域事件相关内容会在后续详细讲解),这就是在边界之外使用最终一致性的原则。
为实现微服务内聚合间的解耦,以及满足未来以聚合为单位的微服务组合与拆分需求,应规避跨聚合的领域服务调用和数据库表关联,也就是要通过应用层实现跨聚合的服务调用 。
不过,要牢记 “适合自己的才是最好的”。在系统设计时,必须充分考量项目的实际情况。面对使用便利性、高性能需求、技术能力短板以及全局事务管理等因素时,这些原则并非不可变通,一切都应以解决实际问题为根本出发点。