关于领域驱动设计(DDD)中聚合设计的一些思考

关于DDD的理论知识总结,可参考这篇文章。html

DDD社区官网上一篇关于聚合设计的几个原则的简单讨论:

文章地址:http://dddcommunity.org/library/vernon_2011/,该地址中包含了一篇关于介绍如何有效的设计聚合的一些原则,共3个pdf文件。该文章中指出了如下几个聚合设计的原则:node

  1. 聚合是用来封装真正的不变性,而不是简单的将对象组合在一块儿;
  2. 聚合应尽可能设计的小;
  3. 聚合之间的关联经过ID,而不是对象引用;
  4. 聚合内强一致性,聚合之间最终一致性;

上面这几条原则,做者经过一个例子来逐步阐述。下面我按照个人理解对每一个原则作一个简单的描述。git

聚合是用来封装真正的不变性,而不是简单的将对象组合在一块儿

这个原则,就是强调聚合的真正用途除了封装咱们自己所关心的信息外,最主要的目的是为了封装业务规则,保证数据的一致性。在我看来,这一点是设计聚合时最重要和最须要考虑的点;当咱们在设计聚合时,要多想一想当前聚合封装了哪些业务规则,实现了哪些数据一致性。所谓的业务规则是指,好比一个银行帐号的余额不能小于0,订单中的订单明细的个数不能为0,订单中不能出现两个明细对应的商品ID相同,订单明细中的商品信息必须合法,商品的名称不能为空,回复被建立时必需要传入被回复的帖子(由于没有帖子的回复不是一个合法的回复),等;github

聚合应尽可能设计的小

这个原则,更多的是从技术的角度去考虑的。做者经过一个例子来讲明,该例子中,一开始聚合设计的很大,包含了不少实体,可是后来发现由于该聚合包含的东西过多,致使多人操做时并发冲突严重,致使系统可用性变差;后来开发团队将原来的大聚合拆分为多个小聚合,固然,拆分为小聚合后,原来大聚合内维护的业务规则一样在多个小聚合上有所体现。因此实现了既能解决并发冲突的问题,也能保证让聚合来封装业务规则,实现模型级别的数据一致性;另外,回复中的一位道友“殇、凌枫”提到,聚合设计的小还有一个好处,就是:业务决定聚合,业务改变聚合。聚合设计的小除了能够下降并发冲突的可能性以外,一样减小了业务改变的时候,聚合的拆分个数,下降了聚合大幅重构(拆分)的可能性,从而能让咱们的领域模型更能适应业务的变化。数据库

聚合之间经过ID关联

这个原则,是考虑到,其实聚合之间无需经过对象引用的方式来关联;架构

  1. 首先经过引用关联,会致使聚合的边界不够清晰,若是经过ID关联,因为ID是值对象,且值对象正好是用来表达状态的;因此,可让聚合内只包含只属于本身的实体或值对象,那这样每一个聚合的边界就很清晰;每一个聚合,关心的是本身有什么信息,本身封装了什么业务规则,本身实现了哪些数据一致性;
  2. 若是经过引用关联,那须要实现LazyLoad的效果,不然当咱们加载一个聚合的时候,就会把其关联的其余聚合也一块儿加载,而实际上咱们有时在加载一个聚合时,不须要用到关联的那些聚合,因此在这种时候,就给性能带来必定影响,不过幸亏咱们如今的ORM都支持LazyLoad,因此这点问题相对不是很大;
  3. 你可能会问,聚合之间若是经过对象引用来关联,那聚合之间的交互就比较方便,由于我能够方便的直接拿到关联的聚合的引用;是的,这点是没错,可是若是聚合之间要交互,在经典DDD的架构下,通常能够经过两种方式解决:1)若是A聚合的某个方法须要依赖于B聚合对象,则咱们能够将B聚合对象以参数的方式传递给A聚合,这样A对B没有属性上的关联,而只是参数上的依赖;通常当一个聚合须要直接访问另外一个聚合的状况每每是在职责上代表A聚合须要通知B聚合作什么事情或者想从B聚合获取什么信息以便A聚合本身能够实现某种业务逻辑;2)若是两个聚合之间须要交互,可是这两个聚合自己只须要关注本身的那部分逻辑便可,典型的例子就是银行转帐,在经典DDD下,咱们通常会设计一个转帐的领域服务,来协调源帐号和目标帐号之间的转入和转出,但源帐号和目标帐号自己只须要关注本身的转入或转出逻辑便可。这种状况下,源帐号和目标帐号两个聚合实例不须要相互关联引用,只须要引入领域服务来协调跨聚合的逻辑便可;
  4. 若是一个聚合单单保存另外的聚合的ID还不够,那是否就须要引用另外的聚合了呢?也没必要,此时咱们能够将当前聚合所须要的外部聚合的信息封装为值对象,而后本身聚合该值对象便可。好比经典的订单的例子就是,订单聚合了一些订单明细,每一个订单明细包含了商品ID、商品名称、商品价格这三个来自商品聚合的信息;此时咱们能够设计一个ProductInfo的值对象来包含这些信息,而后订单明细持有该ProductInfo值对象便可;实际上,这里的ProductInfo所包含的商品信息是在订单生成时对商品信息的状态的冗余,订单生成后,即使商品的价格变了,那订单明细中包含的ProductInfo信息也不会变,由于这个信息已经彻底是订单聚合内部的东西了,也就是说和商品聚合无关了。
  5. 实际上经过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内的版块聚合根无需维护总帖子数这个统计信息了,总帖子数会在查询端的数据库独立维护;dom

从聚合和哲学的角度思考,为何须要状态?

聚合的角度

首先,什么是状态?很简单,好比一个商品的库存信息,那么该库存信息有一个商品的数量这个属性,表示当前商品在库存中还有多少件;那么咱们为何须要记录该属性呢?也就是为何须要记录这个状态呢?由于有业务规则的存在。以这个例子为例,由于存在“商品的库存不能为负数”这样的一个业务规则,那这个规则若是要能保证,首先必须先记录商品的库存数量;由于商品的库存数量是会随着商品的卖出而减小的,而减小就是经过:Product.Count = Product.Count - 1这样的逻辑运算来实现;这个逻辑运算要能运行的前提就是商品要有库存信息。从这个例子咱们不难理解,一个聚合根的不少状态,不是平白无辜设计上去的,而是某些业务规则潜在的要求,必需要设计这些状态才能实现相应的业务规则;这样的例子还有不少,好比银行帐号的余额不能小于0,致使咱们的银行帐号必需要设计一个当前余额的属性;异步

另一个缘由是,看起来像是废话,呵呵。就是:由于咱们关心这些信息,因此须要设计在当前聚合上;好比,以一个论坛的帖子为例,做为一个帖子,咱们一般都会关心帖子的标题、描述、发帖人、发帖时间、所属版块(若是论坛有版块这个概念的话);因此,咱们就会在帖子聚合根上设计出这些属性,以表达咱们所关心的这些信息的状态;

哲学的角度

下面在从偏哲学的角度表达一下对象的概念吧:

人类永远没法认识完整的事物,由于咱们认识到的老是事物的某一方面。咱们所说的对象其实是客观事物在人头脑里的反应,而事物则是不因人的认识发生改变的客观存在。一样一根铁棒,在钢材生产厂家看来,它是成品;在机械加工厂家看来,它是原料;在废品站看来,他是商品。成品、原料、商品,这三者拥有不一样的属性,有本质的不一样。为何同一事物在不一样人的眼里就大相径庭了呢?这是由于咱们老是取对咱们有用的方面来认识事物。当这根铁棒做为商品时,它的原料属性依然存在,只是咱们不关心了。
 
因此,总结出来就是,由于咱们关心一个对象的某些方面,因此咱们才会为他设计某些状态属性;

关于聚合的设计的一些思考

上面只是简单提到,聚合的设计应该多考虑它封装了哪些业务规则这个问题。下面我想再多讲一点个人一些想法:

关于GRASP九大模式中的最重要模式:信息专家模式

仍是以论坛的帖子为例,建立一个帖子时,有一个业务规则,那就是帖子的发帖人、标题、描述、所属板块(若是论坛有板块这个概念的话)都不能为空或无效的值,由于这些信息只要有任何一个无效,那就意味着被建立出来的帖子是无效的,那就是没有保证业务规则,也就没办法谈领域模型的数据一致性了;若是像以往的三层贫血架构,那帖子只是一个数据的载体,不包含任何业务规则,帖子会先被构造一个空的帖子对象出来,而后咱们给这个空帖子对象的某些属性赋值,而后保存该帖子对象到数据库;这种设计,帖子对象只是一个数据的容器,它彻底控制不了本身的状态,由于它的状态都是被别人(如service)去修改的;这样的设计,至关因而没有把业务规则封装在业务对象内部,而是转移到了外部service中,虽然这样一般也没问题,事实上咱们大部分人都一直在这么干,由于这样干写代码很随意,也很高效,呵呵。

GRASP九大模式中有一个面向对象的模式叫信息专家模式,不知道你们有了解过没有,该模式的描述是:将职责分配给拥有执行该职责所需信息的对象;这个模式告诉咱们,若是一个对象负责维护一些信息,那它就有职责维护好这些信息。体现到对象的属性上,那就是这个对象的属性不能被外部随便更改,对象本身的属性必须本身负责维护修改。构造函数和普通的方法都会改变对象的状态,因此,咱们对构造函数和对象普通的公共方法,都要秉持这个原则;这点很是重要,不然,若是像贫血模型那样,那对象就不叫对象了,而只是一个普通的容纳数据的容器而已,和数据库里的一条记录也无本质差异了。实际上,在我看来,这也是DDD中的聚合区别于贫血模型中的实体的最大的地方。聚合不只有状态,还有严格维护好本身状态的各类方法,包括构造函数在内;而贫血模型,则只有状态,没有行为;

关于DDD中一个领域对象是不是聚合根的考虑

这个问题,没有很是清晰的放之四海而皆准的肯定方法,个人想法是:

  1. 首先从咱们对领域的最基本的常识方面的理解去思考,该对象是否有独立的生命周期,若是有,那基本上是聚合根了;
  2. 若是领域内的一个对象,咱们会在后台有一个独立的模块去管理它,那它基本上也是聚合根了;
  3. 是否有独立的业务场景会去建立或修改一个对象;
  4. 若是对象有全局惟一的标识,那它也是聚合根了;
  5. 若是你不能肯定一个对象是不是聚合根的的时候,就先放一下,就先假定它是聚合根也无妨,而后能够先分析一下你已经肯定的那些聚合根应该具体聚合哪些信息;也许等你分析清楚其余的那些聚合的范围后,也推导出了你以前不肯定是不是聚合根的那个对象是否应该是聚合根了呢。

关于一个聚合内应该聚合哪些信息的思考

  1. 把咱们所须要关心的属性设计进去;
  2. 分析该聚合要封装和实现哪些业务规则,从而像上面的例子(商品库存)那样推导出须要设计哪些属性状态到该聚合内;
  3. 若是咱们在建立或修改一个对象时,老是会级联建立或修改一些级联信息,好比在一个任务系统,当咱们建立一个任务时,可能会上传一些附件,那这些附件的描述信息(如附件ID,附件名称,附件下载地址)就应该被聚合在任务聚合根上;
  4. 聚合内只须要值对象和内部的实体便可,不须要引用其余的聚合根,引用其余的聚合根只会让当前聚合的边界模糊,对其余聚合根的引用应该经过ID关联;
  5. 聚合内的实体和值对象应该具备相同的生命周期,整个聚合是一个总体,从外部看就像是一个对象同样,聚合应该遵循同生共死的原则;

关于如何更合理的设计聚合来封装各类业务规则的思考

这一点在最上面的几个原则中,实际上已经提到过一点,那就是尽可能设计小聚合,这里的出发点主要是从技术的角度去思考,为了下降对公共对象(大聚合)的并发修改,从而减少并发冲突的可能性,从而提升系统的可用性(由于系统用户不会常常由于并发冲突而致使它的操做失败);关于这一点,我还想再举几个例子,来讲明,其实要实现各类业务规则,能够有多种聚合的设计方式,大聚合只是其中一种;

好比,帖子和回复,你们都知道一个帖子有多个回复,没有帖子,回复就没有意义;因此不少人就会认为帖子应该聚合回复;但实际上不须要这样,若是你这样作了,那对于一个论坛来讲,同一个帖子被多我的同时回复的可能性是很是高的,那这样的话,多我的同时回复一个帖子,就会致使多我的同时修改同一个帖子对象,那就致使你们都回复不了,由于会有并发冲突或者数据库事务的等待超时,由于你们都在修改同一个帖子聚合根;实际上若是咱们从业务规则的角度去思考一下,那能够发现,其实帖子和回复之间,只有一个简单的规则,那就是回复一旦被建立,那他所对应的帖子不能被修改便可;这样的话,要实现这个规则其实很简单,把回复做为聚合根,而后把帖子传入回复聚合根的构造函数,而后回复保存帖子ID,而后回复将帖子ID设置为不容许外部修改(private set;便可),这样咱们就实现了这个业务规则,同时还作到了多人同时推一个帖子回复时,不会对同一个帖子对象就并发修改,而是每一个回复都是并行的往数据库插入一条回复记录便可;

因此,经过这个例子,咱们发现,要实现领域模型内的各类业务规则,方法不止一种,咱们除了要从业务角度考虑对象的内聚关系外,还要从技术角度考虑,可是无论从什么角度考虑,都是以实现所要求的业务规则为前提;

从这个例子,咱们其实还发现了另一件有意义的事情,那就是一个论坛中,发表帖子和发表回复是两个独立的业务场景;一我的发表了帖子,而后可能过了一段时间,另外一我的对该帖子发表了回复;因此将帖子和回复都设计为独立的很容易理解;这里虽然帖子和回复是一对多,回复离开帖子确实也没意义,可是将回复设计在帖子内没任何好处,反而让系统的可用性下降;相反,像上面提到的关于建立任务时同时上传一些附件的例子,虽然一个任务也是对应多个附件信息,可是咱们发现,人物的附件信息老是随着任务被建立或修改时,一块儿被修改的。也就是说,咱们没有独立的业务场景须要独立修改任务的某个附件信息;因此,没有必要将任务的附件信息设计为独立聚合根;

ENode框架对聚合设计和聚合之间交互的支持

enode提供了一个基于DDD+CQRS+Event Sourcing+In Memory+EDA这些技术的应用开发架构;

  1. enode在框架层面就限制了一个command只能修改一个聚合根,这就杜绝了咱们使用Unit of Work的模式来以事务的方式来一次性修改多个聚合根;
  2. enode提供了可靠的原子操做和并发冲突检测机制,来保证对单个聚合的操做的强一致性;
  3. enode提供了可靠的事件机制,来保证咱们的domain中的聚合之间数据交互能够经过事件异步通讯的方式来实现聚合之间的最终一致性;若是有些复杂业务场景是一个流程,那咱们能够经过Process+Process Manager的思想来实现流程状态的跟踪和流程的流转;
  4. enode由于基于domain event,因此,咱们的聚合根不须要引用,每一个聚合根只须要负责本身的状态更新,而后更新完后产生相应的domain event便可,这本质就是就是实现了:Don’t Ask, Tell这个设计原则;
  5. enode提供了可靠的事件发布机制,能够确保command side和query side的数据最终必定是一致的;
  6. enode提供了in memory的设计,使得咱们的domain能够很是高效的运行,持久化事件不须要事务,获取聚合根直接从in memory获取;
  7. enode提供了不少设计,可让咱们最大化的对不一样的聚合根实例作并行操做,从而提升整个系统的吞吐量;

使用enode,将会迫使你思考如何设计聚合,如何经过流程实现聚合之间的异步交互;迫使你思考如何定义domain event,将领域内的状态更改显式化;迫使你将外部对领域的各类操做显式化,即定义出各类command;迫使你将command side和query side的数据分离和架构分离,技术分离。减小的是,咱们没必要再设计unit of work,没必要设计domain service,没必要让聚合设计各类非第一手的冗余的统计信息;

相关文章
相关标签/搜索