领域驱动模型设计-基础

文章内容摘自:欧创新老师的DDD实战课

01 | 领域驱动设计:微服务设计为什么要选择DDD?

重要知识点整理

  • DDD,一种业务架构设计的方法论,可以用来指导微服务划分,执行系统在业务层面的实现
  • 单机模式:前端,后端,数据库,文件存储服务等都放在一台服务器上,数据库驱动设计,任务总是从设计数据库字段开始
  • 集群模式:将同一个应用部署到多台服务器上以提升服务能力,采用经典的三层架构设计(业务接入层,业务逻辑层,数据库访问层),面向对象设计,耦合高(两个模块关联性很高,例:模块之间的方法相互调用),可扩展性差(扩展时的难易程度,可以指系统功能的扩展,也可指集群应用的扩展),弹性伸缩性差(集群能力的伸缩吗?)
  • DDD 核心思想:通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。
  • DDD 战略设计:从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界
  • 战术设计:从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实
  • DDD战略设计过程:通过事件风暴了解业务全貌,可以使用的分析方法包括(用例分析,场景分析,用户旅程分析)。事件风暴后会产生很多的领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型。如下图:

 

  • 划定领域模型和微服务边界的3个步骤:
  • 梳理业务过程中得到实体和实体值,建立通用的语言
  • 梳理实体之间的业务联系,关联紧密的化为一个聚合
  • 更具业务和语义边界,将多个聚合划分到一个限定上下文内,形成领域模型。限定上下文可以作为微服务的边界
  • 中台:将系统的通用化能力进行打包整合,通过接口的形式赋能到外部系统,从而达到快速支持业务发展的目的。

领域、子域、核心域、通用域和支撑域:傻傻分不清?

重要知识点整理

  1. 领域和子域:领域就是用来确定范围的,范围即边界,DDD 的领域就是这个边界内要解决的业务问题域。领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。
  2. 核心域:业务核心领域,重点挖掘
  3. 通用域:非核心,公用性强,用域也是业务域,不过是可复用的业务域,次要
  4. 支撑域:非核心,支撑域和通用域在战略上基本上是同级的,有时候两者会转换,次要

03 | 限界上下文:定义领域边界的利器

重要知识点整理

  • 从事件风暴建立通用语言到领域对象设计和代码落地的完整过程
  • 有用的经验,设计过程中可以使用一些表格,记录事件风暴和微服务设计过程中产生的领域对象及其属性。例:
    • 需要强调的事:DDD 分析和设计过程中的每一个环节都需要保证限界上下文内术语的统一,在代码模型设计的时侯就要建立领域对象和代码对象的一一映射,从而保证业务模型和代码模型的一致,实现业务语言与代码语言的统一。
  • 限界上下文:具备特定业务含义的场景,同一个东西在不通的场景下会有不同的叫法。正如电商领域的商品一样,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。拿保险举例子,一个保单没付钱之前叫投保单,付完钱后会生成保单,如果修改了保费等信息,又会生成批单。
  • 限界上下文与微服务之间的关系:限界上下文之间的边界就是划分微服务的边界。

04 | 实体和值对象:从领域模型的基础单元看系统设计

重要知识点整理

实体:

  • 在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。
  • 实体是多个属性、操作或行为的载体
  • 事件风暴中,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。
  • 贫血模型:只包含数据,不包含业务逻辑的类
  • 充血模型:既包含数据,又包含业务逻辑的类
  • 实体的代码形态:在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
  • 实体的运行形态:实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。
  • 实体的数据库形态:领域中的实体对象映射到数据库中的持久化对象,可以是1:1,1:n或者是n:1

值对象

  • 定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体,没有标识id。当度量或者描述改变时就是另外一个对象了,它可以和其它的值对象进行等值性比较
  • 值对象的本质:若干个用于描述目的、具有整体概念和不可修改的属性(可以用整体替换的方式来改变)。那这个集合存在的意义又是什么?在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎。

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

 

  • 值对象的业务形态:属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。值对象的代码形态:在实体的代码类中,已属性的形式存在,这个属性的类型是Object类型,Object可以作为零散属性的集合。如下图中的Address类
  • 值对象的运行形态:属性嵌入的方式和序列化大对象的方式。引用单一属性的值对象或只有一条记录的多属性值对象的实体,可以采用属性嵌入的方式嵌入。引用一条或多条记录的多属性值对象的实体,可以采用序列化大对象的方式嵌入。

属性嵌入:

 

序列化大对象:

 

  • 值对象的数据库形态:在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。上图也是数据库形态的例子。
  • 实体和值对象的关系:实体和值对象是微服务底层的最基础的对象,一起实现实体最基本的核心领域逻辑。

05 | 聚合和聚合根:怎样设计聚合?

看系统设计

重要知识点整理

聚合:

  • 聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
  • 领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。
  • 聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“高内聚、低耦合”的。
  • 聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
  • 聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。聚合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务(Repository)来实现;而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务(Service)来组合这两个服务。

聚合根

  • 首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
  • 其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
  • 最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。
  • 怎样设计聚合?

以保险的投保业务场景为例,看一下聚合的构建过程主要都包括哪些步骤。

 

第 1 步:采用事件风暴,根据业务行为,梳理出在投保过程中发生这些行为的所有的实体和值对象,比如投保单、标的、客户、被保人等等。

第 2 步:从众多实体中选出适合作为对象管理者的根实体,也就是聚合根。判断一个实体是否是聚合根,你可以结合以下场景分析:是否有独立的生命周期?是否有全局唯一 ID?是否可以创建或修改其它对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体。

第 3 步:根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出 1 个包含聚合根(唯一)、多个实体和值对象的对象集合,这个集合就是聚合。在图中我们构建了客户和投保这两个聚合。

第 4 步:在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。这里我需要说明一下:投保人和被保人的数据,是通过关联客户 ID 从客户聚合中获取的,在投保聚合里它们是投保单的值对象,这些值对象的数据是客户的冗余数据,即使未来客户聚合的数据发生了变更,也不会影响投保单的值对象数据。从图中我们还可以看出实体之间的引用关系,比如在投保聚合里投保单聚合根引用了报价单实体,报价单实体则引用了报价规则子实体。

第 5 步:多个聚合根据业务语义和上下文一起划分到同一个限界上下文内。这就是一个聚合诞生的完整过程了。

 

 

  • 聚合的一些设计原则

1. 在一致性边界内建模真正的不变条件。聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。

2. 设计小聚合。如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。

3. 通过唯一标识引用其它聚合。聚合之间是通过关联外部聚合根 ID 的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。

4. 在边界之外使用最终一致性。聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦(相关内容我会在领域事件部分详解)。

5. 通过应用层实现跨聚合的服务调用。为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。

 

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值