DDD领域驱动设计的概念解析

DDD领域驱动设计的概念解析

在学习 DDD领域驱动设计 的过程中,这种方法包括特别的抽象概念,晦涩难懂,本文结合作者理解,对其方法论中的一些概念进行解析。
我们按照层次进行概念划分的话,大概是:

  • 事件风暴、领域事件、限界上下文
  • 领域、子域、核心域、通用域、支撑域
  • 聚合、聚合根
  • 实体、值对象
  • 贫血模型、充血模型、失血模型

以上是基本包含所有概念,其实概念就是事物的共同本质特点的抽象,可以理解 DDD 这个方法论,帮我们对业务、类等的相同的特征进行了抽象、找出公共特征进行归类,利用分治思想帮助架构设计,从而保证了模块之间的高内聚和低耦合。

在看本文的同时,希望大家边绘制出各个概念的层级关系、包含关系图等,这样可以清晰了解整个 DDD 的拆分思想。我们在解析的时候安装层级逐步向下分解,方便大家食用。

事件风暴

事件风暴也称为事件建模,类似头脑风暴,通过事件风暴的方法可以快速分析复杂业务领域,完成领域建模的目标。由一群参与者组成,可以是 DDD 专家架构师产品经理项目经理开发人员测试人员等项目团队成员。
从产品愿景、业务场景分析获取:领域事件、实体等。从而进行领域建模,简而言之就是对需求进行建模,然后利用 DDD 的概念进行划分

领域事件

领域事件用来表示领域中发生的事件。举例来说:领域事件可以是业务流程中的一个步骤,这个东西和语义是关联的,如果发生 。。。。。 则 , 当做完。。。。到时候等。满足这些语义的都可以定义为领域事件,常见的用户下单后,发送通知,这种就是典型事件的应用场景。

微服务内的领域事件建议少用,增加复杂性。如果已经采用kafka、mq等消息中间件,领域事件是否还需要持久化? 虽然mq自带持久化,但是中间过程,或者订阅到数据后,数据处理出现问题,数据对账是没有办法的,我们可以对重要数据进行持久化。

有了事件,就会有分布式事务,针对这个问题,我们一般采用最终一致性解决。不过另一半事务失败了怎么了?有异常情况导致数据异常怎么办?

第一个问题,事务失败的情况应该比例是很少的。失败的信息可以采用多次重发的方式,如果这个还解决不了,只能将有问题的数据放到一个问题数据区,人工解决。当然要确保一个前提,要保证数据的时序性,不能覆盖已经产生的数据。
第二个问题,一般来说发布方不会等待订阅方反馈结果。发布方有发布的事件表,订阅方有消费事件表,你可以采用每日对账的方式来发现问题数据。

限界上下文

限界上下文,即是对领域边界的定义,例如:什么才是国内?哪个边界让我们定义了国内与国外?
在业务中,为了避免耦合,我们也需要对业务进行边界定义,方便服务划分,也方便管理。

即: 用通用语言和领域对象,保证在领域之内的一些术语、业务相关对象等,有一个确切的含义,没有二义性。这个边界定义了模型的使用范围,使团队所有成员能够明确的知道什么应该在代码模型中实现,什么不应该在模型中实现。

使用通用语言中的名词可以给领域对象命名,如商品、订单等 对应实体对象。而动词则表示一个事件或动作,如:商品下单、订单已付款 对应领域事件或者命令

设计过程中可以使用一些表格,来记录事件风暴和微服务设计过程中产生的领域对象及其属。比如:领域对象在DDD分层架构中的位置、属性、依赖关系以及与代码模型对象的映射关系等

例如:电商领域的商品,在销售阶段是商品,而在运输过程中就变成了货物。同样一个东西,由于业务的领域不同,赋予了这些术语不同的含义和职责边界。领域边界就是通过限定上下文来定义的。

领域、子域、核心域、通用域和支撑域

领域和子域

其实领域和子域按照正常逻辑来理解可以,领域就是一个范围或者区域,例如国家就是一个领域,而国家内部也可以进行地域划分,划分为不同的省市区,便于进行管理,但是领域之间也有大小不同,在一个领域内部也可以进行划分,例如:子域、子子域等...

而在DDD中,我们要去划分的是业务,进行业务规划,我们把不同的业务划分到不同的领域,领域越大,业务范围就越大,反之相反。称之为子域,每个子域对应更小的问题域或更小的业务范围。

而在划分了领域后,每个领域要进行有序管理,要实现自治,就需要建立领域内部的模型,进而解决相应的业务问题。

核心域、通用域和支撑域

在领域的不断划分过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域:核心域、通用域、支撑域。

  • 通用域

没有太多个性化的诉求,同时被多个子域使用通用功能子域是通用域。例如使用到的通用系统:认证、权限等等

  • 支撑域

不包含公司核心竞争力和通用功能的子域,不具有通用性,例如数据代码的数据字典等系统

  • 核心域

决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。

但是,在划分核心域的时候,往往会出现一些不同观点,举个例子:

不同的人对桃树的理解是不同的。如果这棵桃树生长在公园里,在园丁的眼里,他喜欢的是“人面桃花相映红”的阳春三月,这时花就是桃树的核心域。但如果这棵桃树生长在果园里,对果农来说,他则是希望在丰收的季节收获硕果累累的桃子,这时果实就是桃树的核心域。

核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。
其实领域的核心思想就是将问题逐步细分,来降低业务理解和系统实现的复杂度。通过领域,逐步缩小微服务需要解决的问题域,构建合适的领域模型,而领域模型映射成系统就是微服务了。

实体和值对象

在领域模型中,实体和值对象是组成领域模型的基础单元,所以理解他们 很重要

实体

我们先看看它的定义:拥有唯一标识符,且标识符在经历各种状态变更后仍能保持一致,对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。
在领域模型中,实体是多个属性、操作或者行为的载体,在代码中通常使用 充血模型 实现,与实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。

我们白话一下它,实体就是一种业务定义,在代码中这个实体类是包含很多属性或者方法的,然后这个实体类最重要的不是它的属性,而是它的标识,即我们常说的 ID,而且不管过经过如何处理,这个实体仍然能可以保证它是它自己,即它的 ID 可以保持不变。

比如商品是商品上下文的一个实体,通过唯一的 商品ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品,用户也是同理。
但是在有些复杂的场景下,实体与持久化对象可能是一对多或者多对一的关系. 例如: 用户user角色role 两个持久化对象组成权限实体,一个实体对应两个持久化对象,这是一对多的场景。
再比如,有些场景为了避免数据库的联表查询,提升系统性能,会将 客户信息customer账户信息account 两类数据保存到同一张数据库表中,客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景。

值对象

上面说到,有 ID 标识符的对象叫实体,那没有标识符的属性类叫什么呢? 即:值对象

简单来说,值对象本质上就是一个集合。

  • 那这个集合里面有什么呢?

若干个用于描述目的、具有整体概念和不可修改的属性。

  • 那这个集合存在的意义又是什么?

在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎。

例如:

6B26757305C218A4

人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。这样显示地址相关的属性就很零碎了对不对?现在,我们可以将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,这个集合就是值对象了。

本质上,实体是看得见摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。而值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上是独立出来的,但在逻辑上它仍然是实体属性的一部分,用来描述实体的特征

在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务含义,同时又减少了实体的数量;
在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。

例如:用户和用户的地址信息可以设计到一个表
据说:要发挥对象的威力,就需要优先做领域建模,弱化数据库的作用,只把数据库作为一个保存数据的仓库即可。即使违反数据库设计原则,也可以。

  • 值对象的局限

虽然可以简化数据库实体,但是一个实体一旦嵌入多个值对象则会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就会失去业务含义,不好操作,我们需要对比优劣势,合理使用。

DDD提倡从领域模型设计出发,而不是先设计数据模型。实体和值对象是微服务底层的最基础的对象,一起实现实体最基本的核心领域逻辑。

而在某些业务场景中,地址会被经常修改,地址是作为一个独立对象存在的,这时候它应该设计为实体,比如行政区划中的地址信息维护。

实体和值对象都是领域模型的成员,实体是业务唯一性的载体,是个富对象,包含业务逻辑和唯一标识。值对象是属性的集合,没有唯一标识,只是数据的容器,没有业务逻辑。值对象是实体的一部分,为了简化设计,将部分相关属性抽离成值对象。如果值对象变动,原来的值对象可以直接丢弃。也可以理解为值对象是当时数据的快照,只是当时的状态。值对象过多会导致业务的缺失,影响查询性能。具体哪些属性可以作为值对象存在要具体问题具体分析。

聚合和聚合根

聚合

实体和值对象是基础领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对应实体和状态的描述。
但是实体和值对象都是个体化的对象,他们的行为表现出来都是个体化的动作。

社会是由一个个个体组成,象征着每一个人。随着社会发展,慢慢出现了社团、机构、部门等组织,我们开始从个人变成了组织的一员,大家可以协同工作。
领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。

而聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每个聚合对应一个仓储,实现数据的持久化。聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。

聚合在 DDD 中属于领域层,领域层包含多个聚合,共同实现核心业务逻辑。跨多个实体的业务逻辑通过领域服务实现,跨多个聚合服务通过应用服务来实现。
比如:有的业务场景需要同一个聚合的A和B两个实体共同完成,我们就可以将这段代码用领域服务实现;而有的业务需要聚合C和聚合D实现,我们就可以用应用服务实现

聚合根

聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合,实体之间数据不一致的问题。如果把聚合比作组织,那么聚合根就是这个组织的负责人,这个组织的管理者。聚合根也称为根实体,它不仅是实体,还是聚合的管理者

首页它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。其次他作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。

在聚合之间,他还是聚合对外的接口人,以 聚合根ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协作。也就是说,聚合之间通过 聚合根ID 关联引用,如果需要访问聚合内部的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体

一个微服务可以是多个聚合,也可以是一个聚合,为了高性能

如何设计聚合

  1. 采用事件风暴,根据业务行为,梳理出在投保过程中发生这些行为的所有实体和值对象,比如:客户,行为
  2. 从众多实体中选出合适作为对象管理者的根实体,也就是聚合根。如何选择聚合根:是否有独立的生命周期?是否有全局唯一ID?是否可以创建或者修改其他对象?是否有专门模块来管理这个实体?
  3. 根据业务单一原则和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出一个包含聚合根、多个实体和值对象的对象集合,这个集合就是聚合
  4. 在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型
  5. 多个聚合根根据业务语义和上下文一起划分到同一个限界上下文内

聚合设计原则

  1. 在一致性边界内建模真正不变的条件。聚合是用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现数据的一致性
  2. 设计小聚合。如果聚合设计过大,导致实体之间过于复杂,高频操作时出现并发或者数据库锁,导致系统性能变差
  3. 通过唯一标识符引用其他聚合。聚合之间是通过关联外部聚合根ID的方式引用,而不是直接对象引用的方式
  4. 在边界之外使用最终一致性。聚合内数据一致性,而聚合之间数据最终一致性。在一次事务中,最多更改一个聚合的状态。如果一个业务操作涉及到多个聚合状态的更改,应用采用领域事件的方式异步修改相关聚合,实现聚合之间的解耦
  5. 通过应用层实现跨聚合的服务调用

原则也不是不能突破,可以根据业务调整,这里只是给出方案

贫血模型、充血模型、失血模型

  • 失血模型:模型仅仅包含数据的定义和getter/setter方法,业务逻辑和应用逻辑都放到服务层中。这种类在Java中叫POJO,在.NET中叫POCO
  • 贫血模型:贫血模型中包含了一些业务逻辑,但不包含依赖持久层的业务逻辑。这部分依赖于持久层的业务逻辑将会放到服务层中。可以看出,贫血模型中的领域对象是不依赖于持久层的。
  • 充血模型:充血模型中包含了所有的业务逻辑,包括依赖于持久层的业务逻辑。所以,使用充血模型的领域层是依赖于持久层,简单表示就是 UI层->服务层->领域层<->持久层。
  • 胀血模型:胀血模型就是把和业务逻辑不相关的其他应用逻辑(如授权、事务等)都放到领域模型中。我感觉胀血模型反而是另外一种的失血模型,因为服务层消失了,领域层干了服务层的事,到头来还是什么都没变。

7F91D8AD498BCD60

您的支持是对我最大的鼓励!

发表于: 作者:憧憬。
关注互联网以及分享全栈工作经验的原创个人博客和技术博客,热爱编程,极客精神
Github 新浪微博 SegmentFault 掘金专栏