设计原则
在一致性边界内建模真正的不变条件
要从限界上下文中发现聚合,我们需要了解模型中真正的不变条件。这样才能决定什么样的对象可以放在一个聚合。
不变条件表示一个业务规则,该规则应该总是保持一致。存在多种类型的一致性:
- 事务一致性
要求立即性和原子性 - 最终一致性
在讨论不变条件时,我们讨论的是事务一致性。我们可能有以下不变条件:
c = a + b
当a等于2, b等于3时,c必定等于5。根据这条规则,如果c不为5,那么我们便违背了系统的不变条件。为了保持c的一致性,我们应该在模型中为这些属性设计了 一个边界:
AggregateTypel ( int a; int b; int c; operations ...
聚合边界之内的所有内容组成了一套不变的业务规则,任何操作都不能违背这些规则。边界之外的任何东西与该聚合都是不相关的。因此,聚合表达 了与事务一致性边界相同的意思(在该例中,AggregateTypel拥有3个int类型的属 性,任何聚合都可拥有不同类型的属性)。
聚合用来封装真正的不变性,而非简单地组合对象。聚合内有一套不变的业务规则,各实体和值对象按统一业务规则运行以实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现高内聚的原因。
设计小聚合
如果聚合设计过大,聚合会因为包含过多实体,导致实体间管理复杂,高频操作时会出现并发冲突或数据库锁,即便我们可以保证事务的成功执行,它依然有可能限制系统的性能和可伸缩性。
小聚合设计则可降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务变化。
那么,这里的“小”是什么意思呢?最极端的情况是,一个聚合只拥有全局标识和单个属性,当然,这并不是推荐做法(除非这正是需求所在)。好的做法是使用根实体(Root Entity)来表示聚合,其中只包含最小数量的属性或值类型属性。这里的“最小数量”表示所需的最小属性集合,不多也不少。
哪些属性是所需的呢?简单的答案是:那些必须与其他属性保持一致。比如,一个Product拥有name和 description属性,它们需要保持一致,将它们放在两个不同的聚合中显然无意义。当我们修改name,很可能也会同时修改 description,如果你只修改其一,很可能是在修改语法上的错误或使description能够更匹配name。
在聚合中,若认为有些被包含的部分应该建模成实体,怎么办?首先思考该部分是否会随着时间而改变或该部分是否能被全部替换。若可被全部替换,请将其建模成值对象,而非实体。很多情况下建模成实体的概念都可重构成值对象。优先选用值对象并非意味着聚合就是不变的,因为当值对象属性被替换成其他值时,根实体也就随之改变。
将聚合的内部建模成值对象有很多好处。据所选用持久化机制,值 对象可随根实体而序列化,而实体则需单独存储区域予以跟踪。
实体还会带来某些不必要操作,比如,在使用Hibernate时,需对多表联合查询。对单表读取快得多,而使用值对象也更方便安全。由于值对象不变,测试也相对简单。
小聚合不仅有性能和可伸缩性上的好处,它还有助于事务成功执行,即可减少事务提交冲突。系统的可用性也得到了增强。在你的领域中,迫使你设计大聚合的不变条件约束并不多。当你遇到这样的情况时,可以考虑添加实 体或者是集合,但无论如何,我们都应该将聚合设计得尽量小。
通过唯一标识引用其它聚合
聚合之间是通过关联外部聚合根ID的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。
在边界之外使用最终一致性
聚合内数据强一致性,而聚合间数据最终一致性。
在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合间解耦。
在不持有对象引用的情况下,不能修改其他聚合,因此我们可以避免在同一个事务中修改多个聚合。但这种方式的缺点在于限制性太强,因为在领域模型中我们总需要对象之间的关联关系来完成一些任务。
那么,此时我们应该怎么办呢?
通过应用层实现跨聚合的服务调用
为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。
总结
聚合的特点
高内聚、低耦合,它是领域模型中最底层的边界,可作为拆分微服务的最小单位,但不推荐过度拆分。在对性能有极致要求的场景中,聚合可独立作为一个微服务,以满足版本的高频发布和弹性伸缩要求。
一个微服务可包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。有了该逻辑边界,在微服务架构演进时就可以聚合为单位进行拆分和组合。
聚合根的特点
聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调,聚合根与聚合根之间通过ID关联的方式实现聚合之间的协同。
实体的特点
有ID标识,通过ID判断相等性,ID在聚合内唯一即可。状态可变,它依附于聚合根,其生命周期由聚合根管理。实体一般会持久化,但与数据库持久化对象不一定是一对一的关系。实体可引用聚合内的聚合根、实体和值对象。
值对象的特点
无ID,不可变,无生命周期,用完即扔。值对象之间通过属性值判断相等性。它的核心本质是值,是一组概念完整的属性组成的集合,用于描述实体的状态和特征。值对象尽量只引用值对象。