这个原则,就是强调聚合的真正用途除了封装我们本身所关心的信息外,最主要的目的是为了封装业务规则,保证数据的一致性。在我看来,这一点是设计聚合时最重要和最需要考虑的点;当我们在设计聚合时,要多想想当前聚合封装了哪些业务规则,实现了哪些数据一致性。所谓的业务规则是指,比如一个银行账号的余额不能小于0,订单中的订单明细的个数不能为0,订单中不能出现两个明细对应的商品ID相同,订单明细中的商品信息必须合法,商品的名称不能为空,回复被创建时必须要传入被回复的帖子(因为没有帖子的回复不是一个合法的回复)等;
这个原则,更多的是从技术的角度去考虑的。作者通过一个例子来说明,该例子中,一开始聚合设计的很大,包含了很多实体,但是后来发现因为该聚合包含的东西过多,导致多人操作时并发冲突严重,导致系统可用性变差;后来开发团队将原来的大聚合拆分为多个小聚合,当然,拆分为小聚合后,原来大聚合内维护的业务规则同样在多个小聚合上有所体现。所以实现了既能解决并发冲突的问题,也能保证让聚合来封装业务规则,实现模型级别的数据一致性;另外,回复中的一位道友“殇、凌枫”提到,聚合设计的小还有一个好处,就是:业务决定聚合,业务改变聚合。聚合设计的小除了可以降低并发冲突的可能性之外,同样减少了业务改变的时候,聚合的拆分个数,降低了聚合大幅重构(拆分)的可能性,从而能让我们的领域模型更能适应业务的变化。
这个原则,是考虑到,其实聚合之间无需通过对象引用的方式来关联;
这个原则主要的背景是:如果用CQRS+Event Sourcing的架构来实现DDD,那聚合之间因为通过Domain Event(领域事件)来实现交互了,所以同样也不需要聚合与聚合之间的对象引用,同时也不需要领域服务了,因为领域服务已经被Process(流程聚合根)和Process Manager(流程管理器,无状态)所替代。流程聚合根,负责封装流程的当前状态以及流程下一步该怎么走的逻辑,包括流程遇到异常时的回滚处理逻辑;流程管理器,无状态。负责协调流程中各个参与者聚合根之间的消息交互,它会接受聚合根产生的domain event,然后发送command。另外一方面,由于CQRS的引入,使得我们的domain只需要处理业务逻辑,而不需要应付查询相关的需求了,各种查询需求专门由各种查询服务实现;所以我们的domain就可以非常瘦身,仅仅只需要通过聚合根来封装必要的业务规则(保证聚合内数据的强一致性)即可,然后每个聚合根做了任何的状态变更后,会产生相应的领域事件,然后事件会被持久化到EventStore,EventStore用来持久化所有的事件,整个domain的状态要恢复,只需要通过Event Sourcing的方式还原即可;另外,当事件持久化完成后,框架会通过事件总线将事件发布出去,然后Process Manager就可以响应事件,然后发送新的command去通知相应的聚合根去做必要的处理;
上面这个过程可以在任何一个CQRS的架构图(包括enode的架构图)中找到,我这里就不贴图了。enode中对经典的转账场景用这种思路实现了一下,有兴趣可以去下载enode源代码,然后看一下其中的BankTransferSample这个例子就清楚了。另外,因为事件的响应和Command的发送是异步的,所以,这种架构下,聚合根的交互是异步的;
需要再次强调的一点是,聚合如果只需要关注如何实现业务规则而不需要考虑查询需求所带来的好处,那就是我们不需要在domain里维护各种统计信息了,而只要维护各种业务规则所潜在的必须依赖的状态信息即可;举个例子,假如一个论坛,有版块和帖子,以前,我们可能会在版块对象上有一个帖子总数的属性,当新增一个帖子时,会对这个属性加1;而在CQRS架构下,domain内的版块聚合根无需维护总帖子数这个统计信息了,总帖子数会在查询端的数据库独立维护;
先,什么是状态?很简单,比如一个商品的库存信息,那么该库存信息有一个商品的数量这个属性,表示当前商品在库存中还有多少件;那么我们为什么需要记录该属性呢?也就是为什么需要记录这个状态呢?因为有业务规则的存在。以这个例子为例,因为存在“商品的库存不能为负数”这样的一个业务规则,那这个规则如果要能保证,首先必须先记录商品的库存数量;因为商品的库存数量是会随着商品的卖出而减少的,而减少就是通过:Product.Count = Product.Count - 1这样的逻辑运算来实现;这个逻辑运算要能运行的前提就是商品要有库存信息。从这个例子我们不难理解,一个聚合根的很多状态,不是平白无辜设计上去的,而是某些业务规则潜在的要求,必须要设计这些状态才能实现相应的业务规则;这样的例子还有很多,比如银行账号的余额不能小于0,导致我们的银行账号必须要设计一个当前余额的属性;
另外一个原因是,看起来像是废话,呵呵。就是:因为我们关心这些信息,所以需要设计在当前聚合上;比如,以一个论坛的帖子为例,作为一个帖子,我们通常都会关心帖子的标题、描述、发帖人、发帖时间、所属版块(如果论坛有版块这个概念的话);所以,我们就会在帖子聚合根上设计出这些属性,以表达我们所关心的这些信息的状态;
下面在从偏哲学的角度表达一下对象的概念吧:
人类永远无法认识完整的事物,因为我们认识到的总是事物的某一方面。我们所说的对象实际上是客观事物在人头脑里的反应,而事物则是不因人的认识发生改变的客观存在。同样一根铁棒,在钢材生产厂家看来,它是成品;在机械加工厂家看来,它是原料;在废品站看来,他是商品。成品、原料、商品,这三者拥有不同的属性,有本质的不同。为什么同一事物在不同人的眼里就截然不同了呢?这是因为我们总是取对我们有用的方面来认识事物。当这根铁棒作为商品时,它的原料属性依然存在,只是我们不关心了。
所以,总结出来就是,因为我们关心一个对象的某些方面,所以我们才会为他设计某些状态属性;
上面只是简单提到,聚合的设计应该多考虑它封装了哪些业务规则这个问题。下面我想再多讲一点我的一些想法:
还是以论坛的帖子为例,创建一个帖子时,有一个业务规则,那就是帖子的发帖人、标题、描述、所属板块(如果论坛有板块这个概念的话)都不能为空或无效的值,因为这些信息只要有任何一个无效,那就意味着被创建出来的帖子是无效的,那就是没有保证业务规则,也就没办法谈领域模型的数据一致性了;如果像以往的三层贫血架构,那帖子只是一个数据的载体,不包含任何业务规则,帖子会先被构造一个空的帖子对象出来,然后我们给这个空帖子对象的某些属性赋值,然后保存该帖子对象到数据库;这种设计,帖子对象只是一个数据的容器,它完全控制不了自己的状态,因为它的状态都是被别人(如service)去修改的;这样的设计,相当于是没有把业务规则封装在业务对象内部,而是转移到了外部service中,虽然这样通常也没问题,事实上我们大部分人都一直在这么干,因为这样干写代码很随意,也很高效,呵呵。
GRASP九大模式中有一个面向对象的模式叫信息专家模式,不知道大家有了解过没有,该模式的描述是:将职责分配给拥有执行该职责所需信息的对象;这个模式告诉我们,如果一个对象负责维护一些信息,那它就有职责维护好这些信息。体现到对象的属性上,那就是这个对象的属性不能被外部随便更改,对象自己的属性必须自己负责维护修改。构造函数和普通的方法都会改变对象的状态,所以,我们对构造函数和对象普通的公共方法,都要秉持这个原则;这点非常重要,否则,如果像贫血模型那样,那对象就不叫对象了,而只是一个普通的容纳数据的容器而已,和数据库里的一条记录也无本质差别了。实际上,在我看来,这也是DDD中的聚合区别于贫血模型中的实体的最大的地方。聚合不仅有状态,还有严格维护好自己状态的各种方法,包括构造函数在内;而贫血模型,则只有状态,没有行为;
这个问题,没有非常清晰的放之四海而皆准的确定方法,我的想法是:
这一点在最上面的几个原则中,实际上已经提到过一点,那就是尽量设计小聚合,这里的出发点主要是从技术的角度去思考,为了降低对公共对象(大聚合)的并发修改,从而减小并发冲突的可能性,从而提高系统的可用性(因为系统用户不会经常因为并发冲突而导致它的操作失败);关于这一点,我还想再举几个例子,来说明,其实要实现各种业务规则,可以有多种聚合的设计方式,大聚合只是其中一种;
比如,帖子和回复,大家都知道一个帖子有多个回复,没有帖子,回复就没有意义;所以很多人就会认为帖子应该聚合回复;但实际上不需要这样,如果你这样做了,那对于一个论坛来说,同一个帖子被多个人同时回复的可能性是非常高的,那这样的话,多个人同时回复一个帖子,就会导致多个人同时修改同一个帖子对象,那就导致大家都回复不了,因为会有并发冲突或者数据库事务的等待超时,因为大家都在修改同一个帖子聚合根;实际上如果我们从业务规则的角度去思考一下,那可以发现,其实帖子和回复之间,只有一个简单的规则,那就是回复一旦被创建,那他所对应的帖子不能被修改即可;这样的话,要实现这个规则其实很简单,把回复作为聚合根,然后把帖子传入回复聚合根的构造函数,然后回复保存帖子ID,然后回复将帖子ID设置为不允许外部修改(private set;即可),这样我们就实现了这个业务规则,同时还做到了多人同时推一个帖子回复时,不会对同一个帖子对象就并发修改,而是每个回复都是并行的往数据库插入一条回复记录即可;
所以,通过这个例子,我们发现,要实现领域模型内的各种业务规则,方法不止一种,我们除了要从业务角度考虑对象的内聚关系外,还要从技术角度考虑,但是不管从什么角度考虑,都是以实现所要求的业务规则为前提;
从这个例子,我们其实还发现了另外一件有意义的事情,那就是一个论坛中,发表帖子和发表回复是两个独立的业务场景;一个人发表了帖子,然后可能过了一段时间,另一个人对该帖子发表了回复;所以将帖子和回复都设计为独立的很容易理解;这里虽然帖子和回复是一对多,回复离开帖子确实也没意义,但是将回复设计在帖子内没任何好处,反而让系统的可用性降低;相反,像上面提到的关于创建任务时同时上传一些附件的例子,虽然一个任务也是对应多个附件信息,但是我们发现,人物的附件信息总是随着任务被创建或修改时,一起被修改的。也就是说,我们没有独立的业务场景需要独立修改任务的某个附件信息;所以,没有必要将任务的附件信息设计为独立聚合根;