◆ 什么是聚合
聚合包装一组高度相关的对象,作为一个数据修改的单元。
聚合最外层的对象称为聚合根,它是一个实体。聚合根划分出一个清晰的边界,聚合根外部的对象,不能直接访问聚合根内部对象,如果需要访问内部对象,必须首先访问聚合根,再导航到聚合的内部对象。
聚合代表很强的包含关系,聚合内部的对象脱离了聚合,应该是毫无意义的,或不是你真正关注的,它是聚合的一个组成部分。
什么是聚合根?
举个例子:一个绑匪团伙实施绑架.那这个绑匪的头头就是聚合根.他来负责整个绑架过程. 绑架就是聚合
◆ 简化系统设计
在刚开始接触DDD时,我们受到传统数据建模思维影响,根据范式要求设计出多张表,会很自然的每张表映射成一个实体,每个实体都是聚合。
那么这有什么问题?
当把每个表映射成独立的聚合时,我们在思考问题的时候,会把每个表作为独立对等的概念进行思考,从而使你的大脑分不清主次,淹没在错综复杂的表关系中。
现在如果系统有100张数据库表,每张表以任意方式关联,映射成100个聚合。你在进行思考时,以相同方式对待这100个聚合,很快就会头晕目眩。有经验的开发者知道通过切割模块可以降低复杂度,但各个模块之间错综复杂的关系依然存在。
如果通过聚合的方式进行思考,情况则大不相同。把高度相关的概念封装到一个聚合中,并且将聚合中的对象尽量使用值对象建模,不仅可以减少表数量,在概念上也更加简单和清晰。现在假定还是100张表,每5张表映射到一个聚合中,那么具有20个聚合。我们在思考问题时,整个聚合成为一个独立思考的单元,聚合内部的附属对象已经成为二等公民,你并不需要随时想到它们。由于聚合根外部对象只能直接访问聚合根,所以复杂的关系被封装到聚合内部。我们现在只需要考虑聚合根之间的关系,整个系统设计会大幅简化,系统的耦合度得到控制。
由于仓储代表的是聚合的集合,换句话说,每个聚合应该拥有一个仓储。如果每个表都映射为聚合,那么会导致大量的仓储,哪怕采用了依赖注入框架,整个系统的依赖复杂度还是非常高。
◆ 如何设计聚合
问:系统中这么多对象中哪个才是正确的聚合。
答:只要是设计问题,由于每个人理解不同,肯定答案不一样。更有经验的开发人员能够得到更好的设计,更接近于“标准答案”,但那是建立在充分理解的基础上。如果一个高手告诉你某个类应该是聚合,你却没有真正理解他的用意,这种情况可能导致你设计出一个艰涩的系统。所以正确性是因人而异的。
另外,高手告诉你的聚合也不见得是合适的,因为他不一定了解你的业务实际情况,聚合不仅受逻辑上的概念影响,并且还受到并发、性能等因素制约。
下面介绍选择聚合的一般性规律,可以帮助你进行一些决策。
第一步,寻找具有包含或组成关系的相关对象(聚合内的生命周期都是一致的,要么一起生,要么一起死)。
某些对象有附属的子项,比如订单Order和订单项OrderItem,它们具有包含关系,订单包含订单项的集合,或者可以认为一个订单是由N个订单项组成的。
找到的N组相关对象成为聚合的候选,能不能成为聚合需要经过后面的筛选。
第二步,考虑聚合内部的子对象集合,是否需要被聚合根外部的对象直接访问,如果需要,将其从聚合中移出,并建模为独立聚合。
虽然一个对象可能从概念上被另一个对象包含,但如果这种包含关系很弱,一般意味着子对象离开该聚合可能仍然有意义,外界对象希望能够直接和它打交道。
第三步,聚合内部导致并发冲突严重时,进行聚合拆分。
前两步是从概念上选择聚合,但聚合还受到其它因素影响,比如并发、性能等。
通过乐观离线锁可以保证,两次提交的聚合不会发生更新丢失。如果聚合只包含它本身,出现冲突的可能性就很小。但由于聚合中往往包含集合,甚至是多个集合,所以各个集合之间的修改可能导致并发冲突很严重。
比如一个聚合中包含两个实体集合,用户A正在编辑聚合的第一组实体集合,与此同时,用户B 开始编辑同一个聚合的第二组实体集合,第一个人提交成功,第二个人将更新失败。
如果用户经常需要对聚合内的不同集合进行单独编辑,这就说明聚合中的概念可能具有独立性,应该拆分出来。当聚合内部集合经常导致更新失败时,果断进行拆分是必须的。
设计一个大型聚合,除了可能经常导致并发冲突外,还可能导致低下的性能。比如酒店包含不同的房型,每个房型包含不同的价格政策,每种价格政策的价格又不同,价格可能每隔几天都会变化,如果把酒店作为一个大型聚合,把其它都作为集合包含进来,创建一个酒店聚合的开销可能很惊人。
当聚合中的子对象集合的层级超过2级,比如子对象又包含孙对象集合,需要考虑是否会导致并发和性能问题。另外一个聚合中包含子对象集合的数量也需要控制,比如一个聚合包含10个子对象集合,出现冲突的可能性就会很大。还有一个问题是,包含的子对象集合的元素个数也要考虑,比如一个商品,需要记录商品的价格变动历史,由于价格是商品的一个属性,所以可能会把价格变动历史也放到商品中。如果价格经常变动,比如每天2次,一年就会产生700条记录,可以看到,有些子对象集合刚开始数据量不大,但会持续增加,这种情况也需要进行聚合拆分。
如果一个聚合良好表达了一个整体概念,把附属信息都封装起来,并且没有导致并发冲突经常发生,还性能良好,可以认为设计相当成功了,当然,这很不容易。
举例:
一个交易系统:用户购买某个商品生成一笔交易
可以抽象出聚合根:交易,用户和商品是交易的属性
那用户这个对象 那就不是聚合根,而是值对象
有人问:
那随着业务的发展,比如根据交易衍生出等级会员制度,那用户是不是也会变成一个聚合根 ...
参考答案:
聚合根与上下文场景是有关的,比如我们说北京是首都,它是指中国的首都,是中国这个边界上下文的聚合根,中国在我们谈话表达时是隐藏的一个定义,因为别人不会认为北京是日本或者韩国的首都。
上面这个问题将两个上下文场景混同在一起,一个是用户交易场景,一个是用户会员等级场景。
我们只需要找出上下文边界,聚合根也就找到了。
所以上面的场景交易如果需要会员信息,应该是找用户聚合根要,而不是自己去拿。
建议:
如果刚开始把握不准划分聚合,那就暂时不需要聚合,划分实体和值对象,做完后再看业务情况去根据划分原则划分
如果聚合设计得恰当,外部很少会要直接访问聚合内对象,如果有时偶尔需要,通过聚合根返回值对象即可,这是封装原则。
并不是说,以后如果要用到聚合内对象,都得通过聚合根,特别是一些重要的普通实体,还是可以通过仓储Repository获得的。如果这个实体也是另外一个聚合的聚合根,那也方便。
总之,聚合与有界的上下文都是为了封装。
如果对以上内容有想交流的可以: