前篇文章介绍了聚合的作用,也简单介绍了一下聚合的组成。这次分析一下聚合的设计,是设计成聚合还是值对象?
聚合和聚合根
聚合的组成包括了实体和值对象,取其中一个实体作为聚合的聚合根。想修改这个聚合里面的内容,必须通过聚合来修改,聚合内部对外应该是黑盒的存在。
聚合像一棵树,而树的顶点就是聚合根。所以有时候说这个对象是聚合根,是一个聚合,是一个实体,都没问题。从聚合外部看是一个聚合,从聚合内部看是一个聚合根,也是一个特殊的实体。
实体和值对象
实体和值对象的设计是非此即彼的,没有中间状态,因此放在一起对比能更好的理解他们的差异。
实体是具有一个唯一标识的,就是我们常说的 id,即使里面的属性发生变化,还是同一个对象。而值对象是没有唯一标识的,没有id,里面属性的组合就是唯一标识,只要其中属性发生变化,就是一个新的对象。
例如有一台电脑,有开关机状态,使用时长。开关机状态和使用时长是会变化的,但电脑还是那台电脑,因此应设计成实体。又例如电脑的费用,包含了金额和货币单位,不需要标识哪一个费用,只要金额或者货币单位其中一个发生变化,这个费用就会发生变化。
从例子可以看出一般有生命周期的业务对象应设计成实体,而一些常见的属性,用于描述某种特性的,例如颜色,金额,重量等等,一般设计成值对象。
设计成实体还是值对象并不是根据业务对象本身来设计的,而是需要结合上下文,有时还存在两种都可以的情况。一般来说我们会偏向设计成值对象,因为值对象更加简单,不需要维护状态,且事务安全。如果是实体,例如你要修改里面的值,需要先获取原来的实体,修改里面的属性。而值对象则直接替换对象即可。
聚合还是值对象
聚合是一个特殊的实体,因此聚合还是值对象的问题转化成了实体还是值对象,再考虑是否把实体设计成聚合。实体虽然并不等于聚合,但大多数场景实体会被设计成聚合。而值对象则肯定不会是聚合。
举个例子:一辆车里面的轮胎怎么设计,车已被设计成聚合。 这时会有 2个问题,是设计成实体还是值对象? 如果是实体是否应该设计成一个聚合?
有3种方案:
前面提到如何设计需要考虑上下文。不同上下文的设计也不同。
汽车销售部上下文
销售部的作用是把车卖出去,有2个业务特性。
- 并不关心是哪一条轮胎卖出去的,销售部关心的是车,而车胎只要型号,品牌对的上就行了。同一个型号品牌的轮胎换哪个都一样。
- 车和轮胎必须时刻保持 1/4 的关系。
因为轮胎只关心型号和品牌,不关心是具体哪一条轮胎,也就不需要一个id 做唯一标识,只需要用型号和品牌作为唯一标识就行了。可以设计成值对象。
且车和轮胎要保持 1/4 的关系,这个业务约束很强,证明这两个对象关联关系很强,很适合放到聚合里面。因为放到聚合里面的话每次获取聚合都可以对这个约束进行校验,保证业务拿到的就是没问题的聚合。如果是2个聚合,就需要借助领域服务来约束这两个聚合的关系,需要分别拿出2个聚合来校验,就会变得更难维护。
因此在这个上下文里面适合用方案1。
修车厂上下文
修车厂的作用是如果车胎漏气了,车主会开到修车厂,修车厂会把轮胎拆下来,检查并补胎,再安装上去。对于轮胎来说有2个业务特性。
- 这个轮胎修好就必须安回原车,不能说只要型号一样,或者品牌一样,就随便拿条轮胎来换。
- 车和轮胎并不是一直保持1比4的关系,轮胎拆下来之后,可能就会变成 1比3了。
因为车胎从车上拆下来到安装回去,不管里面的属性发生了什么变化,都应该还是那条轮胎。意味着轮胎是有唯一标识的,因此应设计成实体。也就是排除了方案1。
选择方案2还是方案3重点需要考虑实体之间的业务约束,实体之间的关联强弱和业务复杂度。
- 业务约束:车和轮胎并没有很强的业务约束。因此会考虑聚合多一些。
- 实体之间的关联强弱:轮胎的生命并不是随车的,车坏了以后,轮胎还能继续用,可以换到其他车子上面,因此和车的关系并没到"组合",在修车厂内车甚至可以没轮胎,因此只有"依赖"的关系,关系比较弱。
- 业务复杂度:轮胎有自己的业务流程,例如对轮胎补胎,放气打气等。这些只有轮胎参与没有车参与。如果涉及成实体,和车一起作为一个聚合。因为轮胎只能通过车这个聚合来获取,修改。则会导致这些业务流程都需要和车关联起来,会让车这个聚合变得很复杂。
- 技术考虑:因为聚合是整个进行获取和保存的,因此需考虑聚合过大是否会导致性能问题。是否有强一致性 (和业务约束类似)。
从上面的角度看,把轮胎单独设计成一个聚合会更合理一点,因此选择方案3。
二手车市场上下文
再考虑一个场景,如果是二手车市场,会选择哪个方案呢? 相比前面的销售上下文,二手车市场需要唯一标识那个轮胎,有业务约束,但轮胎又没单独的业务流程,则适合设计成方案 2。
总结
聚合的设计分2步
- 实体还是值对象。根据定义,基于是否需要唯一标识判断。
- 实体还是聚合。根据业务约束,对象间强弱关系,业务复杂度,技术考虑。