DDD基本概念!

领域和子域

领域是用来确定范围的,范围即边界,领域就是这个边界内要解决的业务问题域。

子领域:领域可以进一步划分为子领域,把划分出来的多个子领域称为子域。

  • 每个子域对应一个更小的问题域或更小的业务范围。

核心域、通用域和支撑域

核心域:

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

通用域:

  • 没有太多个性化的诉求,同时被多个子域使用的通用功能子域,比如认证、权限等。

支撑域:

  • 既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,例如数据代码类的数据字典等系统。

注意:商业模式的不同会导致核心域划分结果的不同。

通用语言

在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。

通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。

实体

实体的业务形态:

领域模型中的实体是多个属性、操作或行为的载体。

在事件风暴中,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象。

进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。

  • 实体和值对象是组成领域模型的基础单元。

实体的代码形态:

在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。

在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现。

  • 跨多个实体的领域逻辑则在领域服务中实现。

实体的运行形态:

实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。

可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。

由于它们拥有相同的 ID,它们依然是同一个实体。

  • 比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识。
  • 不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。

实体的数据库形态:

与传统数据模型设计优先不同,DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为。

  • 再将实体对象映射到数据持久化对象。

在领域模型映射到数据模型时,一个实体可能对应 0 个、1 个或者多个数据库持久化对象。

大多数情况下实体与持久化对象是一对一。

在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。

  • 比如,基于多个价格配置数据计算后生成的折扣实体。

而在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。

比如,用户 user 与角色 role 两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。

值对象

值对象描述了领域中的一件东西,这个东西是不可变的,它将不同的相关属性组合成了一个概念整体。

值对象的业务形态:

值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。

  • 值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。

值对象的代码形态:

如果值对象是单一属性,则直接定义为实体类的属性。

如果值对象是属性集合,则把它设计为 Class 类。

Class 将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID,会被实体整体引用。

值对象的运行形态:

值对象嵌入到实体的话,有这样两种不同的数据格式,也可以说是两种方式,分别是属性嵌入的方式和序列化大对象的方式。

引用单一属性的值对象或只有一条记录的多属性值对象的实体,可以采用属性嵌入的方式嵌入。

引用一条或多条记录的多属性值对象的实体,可以采用序列化大对象的方式嵌入。

  • 比如,人员实体可以有多个通讯地址,多个地址序列化后可以嵌入人员的地址属性。

值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。

值对象的数据库形态:

在领域建模时,可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量。

在数据建模时,可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。

实体和值对象的关系:

同样的对象在不同的场景下,可能会设计出不同的结果。

有些场景中,地址会被某一实体引用,它只承担描述实体的作用,并且它的值只能整体替换,这时候就可以将地址设计为值对象。

  • 比如收货地址。

在某些业务场景中,地址会被经常修改,地址是作为一个独立对象存在的,这时候它应该设计为实体。

  • 比如行政区划中的地址信息维护。

限界上下文

通用语言定义上下文含义,限界上下文则定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一的含义。

限界上下文用来封装通用语言和领域对象,提供上下文环境。

保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。

这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。

领域边界就是通过限界上下文来定义的

限界上下文和微服务的关系:

理论上限界上下文就是微服务的边界,限界上下文内的领域模型映射到微服务。

  • 限界上下文是微服务设计和拆分的主要依据。

在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。

聚合

领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合

  • 它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。

聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,是数据修改和持久化的基本单元。

  • 每一个聚合对应一个仓储,实现数据的持久化。

聚合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。

  • 跨多个实体的业务逻辑通过领域服务来实现,

  • 跨多个聚合的业务逻辑通过应用服务来实现。

比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,就可以将这段业务逻辑用领域服务来实现。

  • 有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,就可以用应用服务来组合这两个服务。

怎样设计聚合?

DDD领域建模通常采用事件风暴,它通常采用用例分析、场景分析和用户旅程分析等方法。

通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,并梳理领域对象之间的关系。

  • 找出聚合根,找出与聚合根业务紧密关联的实体和值对象,再将聚合根、实体和值对象组合,构建聚合。

聚合的一些设计原则:

在一致性边界内建模真正的不变条件:

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

设计小聚合:

  • 如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂。

    • 高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。
  • 小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。

通过唯一标识引用其它聚合:

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

在边界之外使用最终一致性:

  • 聚合内数据强一致性,而聚合之间数据最终一致性。
  • 在一次事务中,最多只能更改一个聚合的状态。
    • 如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦。

通过应用层实现跨聚合的服务调用:

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

聚合根

如果把聚合比作组织,那聚合根就是这个组织的负责人。

  • 聚合根也称为根实体,它不仅是实体,还是聚合的管理者。

首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。

其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。

最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求。

  • 在上下文内实现聚合之间的业务协同。

也就是说,聚合之间通过聚合根 ID 关联引用。

  • 如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

领域事件

在事件风暴(Event Storming)时,除了命令和操作等业务行为以外,还有一种非常重要的事件。

这种事件发生后通常会导致进一步的业务操作,在 DDD 中这种事件被称为领域事件。

一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

领域事件总体架构

领域事件的执行需要一系列的组件和技术来支撑。

  • 领域事件处理包括:事件构建和发布、事件数据持久化、事件总线、消息中间件、事件接收和处理等。

11