聚合的定义
聚合(以及聚合根):聚合表示一组领域对象(包括实体和值对象),用来表述一个完整的领域概念。而每个聚合都有一个根实体,这个根实体又叫做聚合根。举个例子,一个电脑包含硬盘、CPU、内存条等,这一个组合就是一个聚合,而电脑就是这个组合的聚合根。聚合根是聚合所表述的领域概念的主体,外部对象需要访问聚合内的实体时,只能通过聚合根进行访问,而不能直接访问。
这个定义有一个有趣的约束,“外部访问聚合内实体,只能通过聚合根”。聚合不仅仅是简单的对象组合,其主要的目的是用来封装业务和保证聚合内领域对象的数据一致性。通过聚合根的控制,有利于确保聚合中的对象满足所有固定规则,确保在任何状态发生变化时候聚合作为一个整体满足固定规则。
在UML中,也存在聚合的概念,即has-a的关系,是一种整体与部分的关系,此时整体与部分之间是可分离的用表示一种强的关联关系。其实学习DDD会发现引用了很多OO中的概念,个人感觉在DDD中的概念更多是从一个建模过程来使用这些概念,作为一种分析工具来使用,通过这些工具去形成一个高内聚,低耦合的领域模型。而在UML种中这种概念往往表达一种定义,对设计过程或设计原则并没有太多约束,这样往往设计结果是参差不齐的。从这个角度看,DDD也可以看作是对OO设计的一种更具体化的方案。
聚合希望解决的问题
大多数业务领域中的对象都有十分复杂的联系,以至于最终会形成一个很长、很深的对象引用路径。在某种程度上这—乱状态反映了现实世界,因为现实世界中就很少有清晰的边界,这在软件设计中是一个重要问题。将关联减至最少有助于简化对象之间的遍历,并在某种程度上限制关系的急剧增多。
在具有复杂关联的模型中,要想保证对象更改一致性是很困难的。不仅互不关联的对象需要遵循一些固定规则。而且紧密关联的各组对象也要遵循一些固定规则。然而,过于谨慎的锁定机制又会导致多个用户之间毫无意义地互相干扰,从而使系统不可用。
聚合设计的原则:
- 聚合是用来封装真正的不变性,而不是简单的将对象组合在一起;
这个原则,就是强调聚合的真正用途除了封装我们本身所关心的信息外,最主要的目的是为了封装业务规则,保证数据的一致性。 - 聚合应尽量设计的小;
- 聚合之间的关联通过ID,而不是对象引用;
- 聚合内强一致性,聚合之间最终一致性;
下面会通过《实现领域驱动设计》的例子再理解下聚合设计过程。
聚合的例子
以设计一个Scrum系统为例来说明下聚合的产生过程。
第一次设计:臃肿的聚合:
一个臃肿的聚合
Product被设计为一个巨大的聚合,Product作为聚合根。这个聚合设计看似诱人,实际却不实用,在多用户同时修改这个聚合的情况很常见,这种臃肿的聚合在多用户操作时候很容易出现事务失败的现象,用户越多,问题越明显。
第二次设计:多个聚合

通过将大的Product聚合拆为4个相对较小的聚合,可以解决之前的事务问题。但是对于客户端而言,多少会带来一些不变。对第一次设计的大聚合也有一些其他方法可以解决事务问题,所以大聚合还有哪些其他问题?文中又提出了其他几个原则:
原则一:在一致性的边界之内建模真正的不变条件
这里的不变条件表示一个业务规则,存在多种类型的一致性,事务一致性就是其中之一,当然也存在最终一致性。设计聚合的时候,需要慎重考虑聚合一致性,而不是创建一个对象树。
原则二:设计小聚合
大聚合不仅仅是事务方面问题,即使解决事务问题,在系统性能和可伸缩方面也会存在问题。这里“小”的意思,使用聚合根表示,其中只包含最小数量的属性或值类型属性。最小表示所需的最小属性集,不多也不少,满足业务需要即可。
哪些属性是所需的?简单说是那些必须与其他属性保持一致的属性。例如一个Product拥有name和description,这里的name和description是需要保持一致的。
原则三:通过唯一标识引用其他聚合

这个原则的含义从上图可以看出来,不要直接去引用一个聚合,而应该改为通过聚合标识的值对象进行引用。好处是聚合会变小,而且也会自然形成一种缓加载的机制。
原则四:在边界之外使用最终一致性
任何跨聚合的业务规则都不能总是保持处于最新状态。通过事件处理、批处理或者其他更新机制。我们可以在一定时间之内处理好他方依赖。
可以通过领域事件支持最终一致性。是使用事务一致性还是最终一致性,有一个简单而实用的指导原则。对于一个用例,问问是否应该由执行该用例的用户来保证数据的一致性。如果是,请使用事务一致性。如果需要其他用户或者系统来保证数据一致性,请使用最终一致性。以上原则向我们展示了真正的系统不变条件:那些必须使用事务一致性的不变条件。通过领域来理解问题比纯粹的技术学习更有价值。
对于聚合来说,以上原则是非常重要的。当然由于我们还需要考虑其他因素,这个原则并不见得总是我们的最终选择。
遵循原则和打破原则
有很多因素需要我们做出妥协,比如用户界面上的考虑、技术限制、企业政策等。当然,我们不应该去找各种借口来打破聚合原则。从长远看来,遵循聚合原则对整个项目是有益的。应该尽可能地保证一致性,并且致力于创建高性能的、高可伸缩性的系统。
一些打破原则的理由包括:
- 方便用户界面
- 缺乏技术机制
- 全局事务
- 查询性能
重新思考设计

在做设计的时候,除了考虑以上各种原则外,还需要考虑是否过渡设计了,另外还需要对设计结果进行成本估算、场景的验证和内存消耗的估算。
还可以进一步对设计进行探索,例如如果把Task设计为聚合会怎么样?这样EstimationEntry就从BacklogItem中分离出来了,对于延迟加载会有好处。但是这样就带了了BacklogItem和Task的一致性问题,这可以跟领域专家进行确认。
这样的设计思考过程还可能涉及到技术选择,状态更新的责任分析,是否存在其他Stakeholder分析,这种分析可能会一致持续下去。但是最终需要有一个决策的时机,决策的结果也不意味着就不能再走其他路线。
聚合的实现
有一些有助于增强实现健壮性的原则:
- 创建具有唯一标识的根实体
- 优先使用值对象
- 使用迪米特法则和“告诉而非询问”原则
迪米特法则:强调了 "最小知识"原则。在客户端对象使用服务对象时,它应该尽量少地知道服务对象的内部结构。客户端对象不应该知道任何关于服务对象属性的信息。对于服务对象来说,它只应该提供表层 接口,在接口方法被调用时,它将操作委派给内部方法以完成功能。
对迪米特法则做一个简单的总结:任何对象的任何方法只能调用以下对象中的方法:(1)该对象自身 (2)所传入的参数对象 (3)它所创建的对象 (4)自身所包含的其他对象,并且对那些对象有直接访问权.
告诉而非询问原则:一个对象不应该被告知如何执行操作。对于客户端来 说,这里的"非询问"表示,客户端对象不应该首先询问服务对象,然后根据询问结果调用服务对象中的方法,而是应该通过调用服务对象的公共接口的方式来“告诉”服务对象所要执行的操作。
- 乐观并发
-
避免依赖注入
这里避免依赖注入只是不要在聚合中注入资源库合领域服务,而不是针对其他场景而言的。
资源库(Repository)
是一个独立的层,介于领域层与数据映射层(数据访问层)之间。它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。
对于每种需要进行全局访问的对象,我们都应该创建另一个对象来作为这些对象的提供方,就像是在内存中访问这些对象的集合一样。为这些对象创建一个全局接口以供客户端访问。为这些对象创建添加和删除方法……此外,我们还应该提供能够按照某种指定条件来查寻这些对象的方法……只为聚合创建资源库……
资源库封装了基础设施以提供查询和持久化聚合的操作。这样能够让我们始终聚焦于模型,而把对象的存储和访问都委托给资源库来完成。只有聚合才拥有资源库。资源库也并不是数据库的封装,而是领域层与基础设施之间的桥梁。DDD关心的是领域内的模型,而并非是数据库的操作。理想的资源库对客户(而非开发者)隐藏了内部的工作细节,委托基础设施层来干那些持久化的活,可以是关系型数据库、NOSQL、甚至内存里读取和存储数据。
在使用资源库时候,可以结合其他一些ORM工具来创建实现,同时可以结合Unit Of Work来使用。持久化地机制可以采用“隐式读时复制”或者“隐式写时复制”的方式来隐式地跟踪发生在持久对象中地变化,而不需要客户端自行处理。
资源库(Repository)与数据访问层(DAL)的区别
- Repository是DDD中的概念,强调Repository是受Domain驱动的,Repository中定义的功能要体现Domain的意图和约束,而DAL更纯粹的就是提供数据访问的功能,并不严格受限于Business层。
- 使用Repository,隐含着一种意图倾向,就是 Domain需要什么我才提供什么,不该提供的功能就不要提供,一切都是以Domain的需求为核心;而使用DAL,其意图倾向在于我DAL层能使用的数据库访问操作提供给Business层,你Business要用哪个自己选。换一个Business也可以用我这个DAL,一切是以我DAL能提供什么操作为核心。
引用
关于领域驱动设计(DDD)中聚合设计的一些思考
DDD理论学习系列(10)-- 聚合
DDD理论学习系列(12)-- 仓储
掀起你的盖头来:Unit Of Work-工作单元
网友评论