解析DDD中的聚合对象

对象这个词对我们而言并不陌生。以最常见的面向对象为例,软件系统中的任何事物都被认为是一种对象。而针对如何设计和实现这些对象,也存在一批开发模式。例如,一种传统的做法是从数据的角度来规划对象的组织形式,先设计数据库模型,然后基于数据库模型设计对象,这些对象内部通常只包含数据属性的定义,也就是说整个开发过程是数据驱动(Data Driven)的。


随着DDD思想和方法日渐深入人心。我们认为数据驱动并不是一种理想的对象设计方法,取而代之的应该采用领域驱动(Domain Driven)的方法来设计系统中的对象。为此,DDD专门引入了一个全新的对象,即聚合(Aggregate)对象。那么:

  1. 什么是聚合对象?
  2. 如何设计聚合对象?

这就是我们今天要讨论的内容。

什么是聚合对象?

在DDD中,对象的设计过程是领域驱动(Domain Driven)的,这与数据驱动完全不同。这些领域模型对象内部不仅仅只定义了数据属性,而更多包含了业务逻辑的处理过程。下图展示了数据对象与领域模型对象的这种差异。


和限界上下文被用来划分子域之间的业务边界一样,在领域模型对象中,我们也需要从软件复杂度的角度出发,明确对象之间的边界。现在,假设系统中存在8个对象,那么它们的一种交互方式如下图所示。


从上图中可以看出,原则上这8个对象之间的交互方式最多可以达到28-1次。而一个系统中的对象显然远远不止这个数量级,系统的复杂度会随着对象数量的增多而急剧上升,这也是架构腐化的一个根源。为了降低这种对象交互所带来的复杂度,DDD引入了聚合对象的概念。那么,聚合是如何来做到这一点的呢?

聚合的设计思想实际上很简单,就是尽量减少系统中对象之间的关联关系,简化外部组件对领域模型对象的访问入口,从而降低系统的复杂度。

现在,我们已经明确了DDD中聚合的设计思想,接下来我们来讨论它的组成结构。聚合有两部分组成,分别是聚合根和聚合边界。

  1. 聚合根:聚合对外暴露的访问对象,外部组件只能通过这个聚合根对象实现对聚合的修改和更新
  2. 聚合边界:聚合的业务边界和范围,规定了一个聚合内部应该具备哪些业务逻辑和操作


换句话说,对于一个聚合而言,外部组件只能看到一个聚合根对象,而位于聚合边界内部的其他对象只能通过聚合根进行关联,这种关联操作包括对对象的创建、查询、更新和删除。通过这种固定的规则,我们也确保聚合内部的数据操作具有严格的事务性。我们回到前面讨论的案例,基于聚合思想就可以得到如下图所示的效果图。


可以看到,我们把原本8个对象拆分成了3个聚合对象,每个聚合对象又一个明确的边界,内部包含了一定数量的领域模型对象。而这3个聚合对象之间的交互只能通过各自的聚合根。这样,对象之间的最多交互次数就变成了23-1次,而不是原本的28-1次。

如何设计聚合对象?

现在,我们已经回答了“什么是聚合对象?”这一核心问题。接下来讨论今天的第二个问题,即如何设计聚合对象?

事实上,在DDD中,领域模型对象包括三大类。除了聚合对象之外,还包括实体(Entity)和值对象(Value Object)。


在这三种领域模型对象中,聚合是核心。实体和值对象是聚合的组成部分,而值对象同时也是实体的组成部分,这三种对象之间的组成关系如下图所示。


接下来,我们先从实体对象开始讲起。实体对象是构成聚合对象的基础。事实上,聚合中的聚合根就是一种实体对象。

实体

实体对象和数据对象的区别在于,实体中除了定义了数据属性之外,还包含了业务的状态以及围绕这些状态所产生的生命周期,也就是说它具有可变性。同时,我们也需要使用唯一标识来区分不同的实体对象,也就是说它具有唯一性。


唯一标识(Identity)是实体对象必须具备的一种属性,也是实体与值对象之间核心区别之一。实体对象的唯一标识概念比较好理解,实现起来方法也有很多。而所谓的可变性,指的是实体对象一般都会具备自己的基础业务方法,同时对自身对象的生命周期进行统一的管理和维护。下图展示了一个典型的实体对象的表现形式,这个对象来自于客户系统中的工单管理场景。


值对象

值对象的特征决定了如何分离值对象的方法。与实体对象相比,值对象自身没有状态,所以是一种不可变对象。在设计过程中,通常我们会先识别系统中的实体对象,然后从实体对象中分离出潜在的值对象。下图给出了实现这一过程的一个示例。

 

在上图中,我们首先设计了一个订单对象Order,显然该对象具有唯一标识符orderNumber,所以是一个实体对象。然后我们发现Order对象中包含了收货地址DeliveryAddress,而DeliveryAddress就可以被抽取成一个值对象。因为对于一个收货地址而言,可以被多个订单所共用,不需要具备唯一标识,而且也没有可变性。如果我们需要改变收货地址,通常是新生成一个DeliveryAddress对象。

聚合

介绍完实体和值对象之后,我们通过一个典型的案例来解释聚合建模的实现过程。在日常开发过程中,我们通常都需要对业务功能(Feature)进行评审,然后通过拆分任务(Task)的方式完成工作量评估和排期(Schedule)。在这个业务场景中,一个业务功能可以创建很多任务,同时需要评估出一个排期。通过分析,我们可以识别Feature、Task、Schedule这三个领域模型对象。

那么,如何基于这三个领域模型对象开展聚合设计工作呢?我们有三条基本原则。

  1. 关注聚合内部真正的不变条件
  2. 设计小聚合
  3. 通过唯一标识引用其他聚合

基于以上三条设计原则,我们可以通过下图完成对上述场景的聚合建模。


可以看到,这里我们把Feature和Task设计为聚合对象,并通过两个值对象分别指定他们的唯一标识,而把Plan设计成一个实体对象。

如果你正在开发一个DDD应用程序,那么识别系统中的聚合对象并完成对其的建模是一项必不可少的工作。针对系统中的每一个子域以及上下文边界,我们首先对系统中存储的各种对象进行区分,在实体、值对象的集成上抽象聚合概念,确保边界的完整性和对象访问有效性。

聚合建模方法需要考虑业务场景和需求上下文,有时候并没有标准的设计方案,而是取决于你对业务模型的理解和分析。今天的内容针对聚合的设计思想以及表现形式进行了详细的分析,帮助你更好的把它应用到日常开发过程中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值