领域驱动设计(3)

聚合设计

如果把有界上下文比喻为对土地进行划界,那么在划好界的土地上盖房子就类似于聚合;这些房子中有主要建筑和辅助建筑,是一群房子,而聚合也是一群对象,其中也有主从之分。建筑群与这块土地的关系类似于聚合与有界上下文的关系,聚合是一种领域模型,这种模型的意义取决于它所处的有界上下文,而有界上下文中逻辑一致性这样的核心概念也必须通过聚合等领域模型来体现,这是首要设计原则。

如果说有界上下文解决了领域内的划分,那么聚合就解决了有界上下文内对象之间的划分。所谓划分就是将紧密的放一起,让松散的更加松散,甚至没有关系。从这里能看出DDD的一种收缩趋势,各领域分别向以聚合为核心的方向设计。

聚合设计的概念

聚合是DDD中的一个重要概念,它对外代表的是一个整体,类似于一个大的对象,内部是由有主从之分的很多对象组成的。聚合是一个行为在逻辑上高度一致的对象群,注意,它是一个对象群体的总称。聚合的内部结构如同一棵树,每个聚合都有一个根,其他对象和聚合根之间都是枝叶与树根的关系。

只有“根”能引用或指向其他对象,“根”自身不能被其他任何对象引用;“根”类似团队的小组长,队员都要向其汇报工作。这就是聚合根的设计来源,聚合根拥有自己边界内的数据所有权,以及行为职责的管理权限。

数据和行为两者兼顾的所有权只有聚合才能具有,为什么需要数据和行为两者兼顾呢?通常情况下,数据和行为是分离的,行为在服务中实现,而数据隔离在数据表中,行为通过服务转为SQL语句去操作数据表,这种方式的问题是隔离了行为和数据的紧密逻辑关系。

例如:我们要支持一个提现的交易,需求如下

  1. 借助paypal平台能力,提供基础提现服务。

  1. 根据paypal的回调结果,发出提现交易的失败和成功事件

采用传统的ER分析方法,交易的模型。

  1. 提现交易的数据,记录提现的交易号,交易的状态以及其他的交易关键信息。

这种ER分析法,考虑到行为和数据的有机结合,提现交易创建,成功和失败,都是交易本身,不可以将其分散到外面的服务中去实现。交易数据表会因为一种数据结构而无法加入创建,失败,成功的行为,技术绑架使业务实现变得扭曲,应该用更好的范式来表达业务。类是一种行为和数据相结合的表示方式,那么无疑使用交易类来映射交易这个概念是合适的。交易类有三个方法,创建,失败,成功,还有一些交易的其他私有属性。交易的状态,当调用创建方法后,交易的状态变成以创建,同理,不同的方法对应不同的状态。

注意在这三个方法中是可以加入业务规则的。这种实现方式是直接映射业务领域概念的,但是如果数据和行为相分离,提现服务就只能先查询交易表的状态字段,然后再判断是否符合业务规则。

假设提现交易中有一条业务规则:如果提现发起到paypal回调结果时间超过五分钟,那么无论回调的结果是什么,都判定交易失败。那如何实现呢,按照ER分析法来做,就只能在paypal回调回来的时候,查询一下创建的时间,然后和回调的时间进行对比,最后这个逻辑会遍布在很多的使用的地方,如果逻辑发生改变,例如,干掉这个时间的限制,均以回调的状态为准,那么修改的地方就会非常多,代码非常难以修改,影响面也会非常多。

如果业务逻辑是系统核心,将其散落在各处肯定不是领域驱动的设计。领域驱动设计应该是将业务逻辑视为核心,而且核心只有一个。如图

将数据关系和行为有序地组织成,以业务逻辑为聚合的最高层,才能真正完整地表达业务领域内在逻辑的一致性,这是聚合设计的目的所在。

设计聚合的几种方法

聚合代表一种高度紧密的关系,那么如何从有界上下文中设计这些高聚合的紧密关系呢?

聚合的设计会有几种常见的设计方法

根据领域事件设计聚合

在业务领域中,经常发生的是“活动”和“事实”,“事实”是在“活动”中发生的,发生的“事实”通过领域事件表达,那么,“活动”就使用聚合表达,这样,“事实”发生在“活动”中,就可以表达为:领域事件是在有界上下文的聚合中发生的,如图

前面有关事件风暴的讨论中,主要是通过领域事件发现有界上下文,更进一步,有界上下文发现了,下一步就是有界上下文内的聚合。命令是具体落到聚合根这个对象上,当聚合根根据业务规则或逻辑执行了这个命令,实际上就代表聚合根内部的状态发生了改变,一些事实发生了,聚合根再抛出领域事件。聚合相比有界上下文而言,更能落实在具体代码设计上,如果使用传统SOA架构做比喻,有界上下文的设计代表服务的设计,而聚合的设计代表数据表关系的设计,当然传统数据库设计是先有数据表才有关系表,而DDD的革命性正是在这里——先有关系才有关系内的对象,这也是“对象只有在关系中才有意义”的一个体现。

通过事务边界设计聚合

每当发现领域中有两个元素紧密结合在一起时,就有可能会发现潜在的聚合,因为聚合的特点是紧密关联。可以根据这些元素的存储方式发现并设计聚合。

例如:用户必须在注册之前输入姓名、邮件地址,如果没有这两个输入,就应该无法创建账户。也就是说,如果他们不满足业务上所有这些条件,他们创建账户的请求(即事务)将被拒绝。

事务体现了业务规则,这里的业务规则是:在任何情况下,没有姓名和邮件地址的客户都不应存在于系统中,称此为不变业务规则。

从存储技术角度看,当几个元素需要同时存储时,其中一个元素发生改变需要存储,其他元素的改变也需要同时存储;如果其中一个元素提交失败,则需要回滚撤销所有更改。这个时候,可以考虑这几个元素代表的业务概念是否为一个聚合。

实例解析:提现服务的设计

上一节中,我们有提到我们的提现场景,如果我们用领域事件的方式去设计提现的聚合,那这里发生了哪些命令和事件呢。

在提现上下文中,命令和事件都是针对一个聚合发生的,这个聚合表达了一种逻辑不变性,就是维持“提现申请到提现交易结果”这样的业务关系,这种关系记录了提现发起申请的活动,因此,表达活动中业务关系不变性的叫法,可以称为统一语言是“提现交易”

用户发起提现申请,也可以看作是创建提现交易,是一个命令,聚合执行一定的业务规则检查,执行了这个命令,“创建提现交易已成功”的事件被抛出。

而提现这个交易,描述成业务语义,应该是,一个人因为一个什么事情,发起了一笔提现的交易。

因此,用提现场景的聚合,应该可以描述成如下样子,user 对应的是上面提到人“一个人”,Order 对应的就是“因为一个什么事情”;而聚合更则是这个交易本身。

实体和值对象

在没有设计的朴素情况下,领域模型一般是一个数据对象(DTO等),其中只有setter/getter方法,是一种纯粹的数据结构,然后将很多数据结构的算法操作设计在服务(Service)等专门的接口类中。这样,数据对象作为服务接口方法的参数传入,在服务的方法中被加工。

DDD领域模型=数据结构+操作方法,数据和行为结合在一起才是一个完整的真正业务对象(领域对象),也才能够真正发挥对象封装的作用,这样的对象或类称为“充血模型”,而没有行为方法的纯数据结构的类称为“失血模型”,后者虽然也使用类这个设计符号,但是并没有真正完整地使用类,只是使用类的setter/getter行为,这些行为还是围绕数据进行设置和读取,没有任何业务意义,这种类实际还是数据结构,也称为“贫血模型”。

上面介绍的聚合根对象就应该是一个充血模型,聚合根通过自己的行为和聚合结构维持整个聚合边界的逻辑一致性或不变性约束。

区分开失血模型和贫血模型,有助于认识到数据库中的实体表其实是一种失血模型、一种纯数据结构;通过ORM等工具映射到Javabean,也是一种只有setter/getter的失血模型,这些实体模型并不是DDD中的实体。下面看看DDD中的实体是什么。

实体

一个实体首先是一个“类”,属于一个类别,这是类别的抽象形式,它还拥有自己的内容数据,这些内容数据需要以一个标识来标记。标识是实体的内容抽象,只有类别的区分是不够的,需要对同一种类别下的数据实例进行区分。

“类”是分类标准,而“对象实例”代表类的一个实例,实体的对象实例则需要使用标识标记,而普通的对象实例可能就没有这个要求,实体的对象实例如果没有标识,就很难在仓储或数据库中找到它,如果找不到它,数据也就没有意义了。

实体的标识主要涉及实体对象实例的创建以及它从生到死的生命周期管理,这个过程需要标识来标记,如果没有标识,将无法管理一个个实体对象实例,也就无法管理它们代表的业务数据和逻辑。

值对象

值对象是没有唯一标识的对象,是一堆数据值的容器。这些数据值并不需要或根本就没有共同特征,但是值对象(VO)与数据传输对象(DTO)还是有区别的。

首先,值对象中的数据值一旦被构建,就不能改变,这是不变性的特性,而DTO没有这种约束,这容易导致DTO传输过程中不断添加、修改各种字段。DTO变成一个装载数据的可变长度的容器,虽然给编程带来了方便,但是将可变性带到代码的各个地方,最后DTO进数据库存储时,才发现数据并不是原来想象的那样,至于在哪个环节修改了,就需要不断地跟踪,这种跟踪在复杂软件中也非常复杂。

值对象的不变性克服了DTO的这种缺点,如果希望改变其中的值,可重新构建一个新的值对象,这样有别于原来的对象,也可以使用克隆模型克隆(clone)原来的一些数据,甚至有很多框架支持对象之间的数据克隆。

其次,值对象的构建一般是由聚合根实体负责的,任何聚合外界需要使用聚合内的信息,都需要通过聚合根访问。聚合根不能将自己内部的对象直接奉献给外部,因为一旦被外界修改了,自己都不知道,就可能造成内部逻辑的不一致,就像有外键关联的两个数据表,一个表修改了数据,而另外一个没有修改,这种情况是可怕的,不过因为外键约束的存在,数据库会进行这两个表的原子更新,但是内存中的对象没有这样的技术机制,而需要通过专门的设计来保证,因此不将聚合内部的对象直接暴露给外界是基本原则,外界如果需要一些数据,可以根据聚合内对象构造一个值对象使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值