天天看点

DDD领域驱动设计二、基础概念一、领域和子域二、解核心域、通用域和支撑域三、通用语言和限界上下文四、实体和值对象五、聚合和聚合根六、领域事件七、微服务边界

文章目录

  • 一、领域和子域
  • 二、解核心域、通用域和支撑域
  • 三、通用语言和限界上下文
    • 1、通用语言
    • 2、限界上下文(bounded context)
    • 3、界限上下文和微服务的关系
  • 四、实体和值对象
    • 1、实体
    • 2、值对象
      • 2.1、值对象的优势和局限
  • 五、聚合和聚合根
    • 1、聚合
      • 1.1、聚合和上下文边界
      • 1.2、聚合和领域
    • 2、聚合根
    • 3、怎样设计聚合
      • 3.1、在一致性边界内建模真正的不变条件
      • 3.2、设计小聚合
      • 3.3、通过唯一标识引用其它聚合
      • 3.4、 在边界之外使用最终一致性
      • 3.5、通过应用层实现跨聚合的服务调用
      • 3.6、补充
  • 六、领域事件
    • 数据一致性和解耦
  • 七、微服务边界
    • 1、逻辑边界
    • 2、物理边界
    • 3、代码边界

领域、子域、核心域、通用域、支撑域、通用语言、限界上下文、聚合、聚合根、实体、值对象、领域事件、事件风暴等等,非常多。这些名词,都是关键概念,它们非常抽象难以理解,但却又很重要,是理解 DDD 的核心设计思想和理念的基础。

一、领域和子域

不管是在软件开发中还是在研究其他问题的时候,我们都可以把复杂问题根据某些特性拆分成多个子问题,每个问题只在特定的范围或区域内研究,这个特定的范围或区域就可以称为领域。

当一个领域的问题还是很复杂的时候,我们可以继续拆分,拆分出来的子领域研究的范围或区域就称为 子领域 。

这样的例子有很多,例如,我们研究植物的时候,可以把植物拆分成两大领域:营养器官和生殖器官。

营养器官又分为:根、茎、叶三个子领域;生殖器官分为:花、果实、种子三个子领域。

DDD领域驱动设计二、基础概念一、领域和子域二、解核心域、通用域和支撑域三、通用语言和限界上下文四、实体和值对象五、聚合和聚合根六、领域事件七、微服务边界

当然各个器官还是可以再继续划分,这就是划分子域的过程。

二、解核心域、通用域和支撑域

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

1、决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力,所以可以多投入一些资源。

2、没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域,比如认证、权限等等,这类应用很容易买到,没有企业特点限制,不需要做太多的定制化。

3、还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域,例如数据代码类的数据字典等系统。

划分核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

三、通用语言和限界上下文

在 DDD 领域建模和系统建设过程中,会有很多不同的角色参与进来,包括领域专家、产品经理、项目经理、架构师、开发经理和测试经理等,不同角色对同一个东西可能会有不同叫法,或者有不同的理解。这样交流起来就有障碍,通用语言和限界上下文就是来解决这样的问题的。

1、通用语言

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

确定了通用语言之后,在同个界限上下文中,任何人对同一个东西只能叫同一个名字,包括产品做需求分析和程序员写代码都用一样的名字。

这就和秦王扫六合,统一了全国的计量单位有类似的作用。

DDD 分析和设计过程中的每一个环节都需要保证限界上下文内术语的统一,在代码模型设计的时侯就要建立领域对象和代码对象的一一映射,从而保证业务模型和代码模型的一致,实现业务语言与代码语言的统一。

2、限界上下文(bounded context)

我们知道语言都有它的语义环境,同样一句话在不同的语境下可能是不同的意思,例如:能穿多少穿多少,在不同语境下说这句话会是完全相反的意思。为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD 在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界。

我们可以将限界上下文拆解为两个词:限界和上下文。限界就是领域的边界,而上下文则是语义环境。通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流。

限界上下文用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。

另外,业务的通用语言就有它的业务边界,我们不大可能用一个简单的术语没有歧义地去描述一个复杂的业务领域。限界上下文就是用来细分领域,从而定义通用语言所在的边界。

正如电商领域的商品一样,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。看到这,我想你应该非常清楚了,领域边界就是通过限界上下文来定义的。

总的来说,在一个界限上下文中,一种物品有唯一的一个术语,一个术语也能对应一种物品,但是一种物品过了上下文界限就有可能用其他术语来称谓。

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

理论上限界上下文就是微服务的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。

而领域专家、架构师和开发人员的主要工作就是通过事件风暴来划分限界上下文。限界上下文确定了微服务的设计和拆分方向,是微服务设计和拆分的主要依据。如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。

一个微服务可以存在一个或多个界限上下文,一个界限上下文可以存在一个或多个聚合。

四、实体和值对象

1、实体

实体类主要是作为数据管理和业务逻辑处理层面上存在的类;实体有唯一标识,唯一标识唯一确定一个对象,即使对象的其他属性值都改变了,只要唯一标识没变,就还是原来的实体对象。实体类的主要职责是存储和管理系统内部的信息,它也可以有行为,甚至很复杂的行为,但这些行为必须与它所代表的实体对象密切相关。

领域模型中的实体是多个属性、操作或行为的载体。在事件风暴中,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。你可以这么理解,实体和值对象是组成领域模型的基础单元。

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

2、值对象

值对象的定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。

值对象本质上就是一个集。它将不同的相关属性组合成了一个概念整体,没有唯一标识,是个整体的值,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。

值对象可以是以json格式或者其他格式存在,不能修改里面的部分值,只能整体替换。当集合里面的属性值改变时,可以用另外一个值对象予以替换。

地址是一个整体的概念,把地址拆分成省市区(县)街道(镇)详细地址破坏了地址的整体性。

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

例如:地址是一个整体的概念,把地址拆分成省市区(县)街道(镇)详细地址破坏了地址的整体性。在领域建模时,我们可以把地址作为值对象,人员作为实体,这样就可以保留地址的业务涵义和概念完整性。而在数据建模时,我们可以将地址的属性值嵌入人员实体数据库表中,只创建人员数据库表。这样既可以兼顾业务含义和表达,又不增加数据库的复杂度。

值对象就是通过这种方式,简化了数据库设计,总结一下就是:在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。

另外,也有 DDD 专家认为,要想发挥对象的威力,就需要优先做领域建模,弱化数据库的作用,只把数据库作为一个保存数据的仓库即可。即使违反数据库设计原则,也不用大惊小怪,只要业务能够顺利运行,就没什么关系。

2.1、值对象的优势和局限

把地址作为值对象存储到数据库中明显违反了数据库设计三大范式的第一范式:确保每列的原子性(需要的同学可以看看这篇博客 数据库设计)。那我们为什么要这么用呢,是否没有缺点呢?

值对象采用序列化大对象的方法简化了数据库设计,减少了实体表的数量和表之间的关联,可以简单、清晰地表达业务概念。这种设计方式虽然降低了数据库设计的复杂度,但却无法满足基于值对象的快速查询,会导致搜索值对象属性值变得异常困难。

值对象采用属性嵌入的方法提升了数据库的性能,但如果实体引用的值对象过多,则会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就会失去业务涵义,操作起来也不方便。

所以,你可以对照着以上这些优劣势,结合你的业务场景,好好想一想了。那如果在你的业务场景中,值对象的这些劣势都可以避免掉,那就请放心大胆地使用值对象吧。

有些场景中,地址会被某一实体引用,它只承担描述实体的作用,并且它的值只能整体替换,这时候你就可以将地址设计为值对象,比如收货地址。而在某些业务场景中,地址会被经常修改,地址是作为一个独立对象存在的,这时候它应该设计为实体,比如行政区划中的地址信息维护。但是有时候并不是很好确定,这就要我们根据实际情况结合上面说的优势和局限做出取舍,这也要有一定的经验才能做好。

五、聚合和聚合根

1、聚合

领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。

你可以这么理解,聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。

1.1、聚合和上下文边界

聚合只能有一个聚合根,一个上下文边界可以有多个聚合,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“高内聚、低耦合”的。

1.2、聚合和领域

我们知道领域可以拆分为多个子领域,当拆分到最后不可再拆分时,我们就可以把最后的这个子领域作为聚合。

聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。

DDD模式可以在实体内实现个体业务,但只能处理这个实体本身的业务,这样可以使业务逻辑更加高内聚,这种模式叫充血模型。如果需要夸多个实体的业务逻辑则通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。

比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现;而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。

2、聚合根

聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。

如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。

作为实体,聚合根有实体的一切特征,有实体的属性和业务行为,有独立的生命周期,可以实现自身的业务逻辑。

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

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

3、怎样设计聚合

DDD 领域建模通常采用事件风暴,它通常采用用例分析、场景分析和用户旅程分析等方法,通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,并梳理领域对象之间的关系,找出聚合根,找出与聚合根业务紧密关联的实体和值对象,再将聚合根、实体和值对象组合,构建聚合。

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

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

3.2、设计小聚合

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

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

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

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

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

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

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

以上都是DDD通用的一些设计建议,这些建议不一定都要准守,我们一切以解决实际问题为出发点,在实际项目设计的时候要考虑项目的具体情况,如果面临使用的便利性、高性能要求、技术能力缺失和全局事务管理等影响因素,这些建议是可以不准守的。

3.6、补充

大部分的业务场景我们都可以通过事件风暴,找到聚合根,建立聚合,划分限界上下文,建立领域模型。但也有部分场景,比如数据计算、统计以及批处理业务场景,所有的实体都是独立无关联的,找不到聚合根,也无法建立领域模型。但是它们之间的业务关系是非常紧密的,在业务上是高内聚的。我们也可以将这类场景作为一个聚合处理,除了不考虑聚合根的设计方法外,其它诸如 DDD 分层架构相关的设计方法都是可以采用的。

六、领域事件

在事件风暴(Event Storming)时,我们发现除了命令和操作等业务行为以外,还有一种非常重要的事件,这种事件可以是业务流程的一个步骤,或者是满足某些条件后会导致进一步的业务操作,例如:密码联系输错三次,就触发锁定账户的动作,在 DDD 中这种事件被称为领域事件。

数据一致性和解耦

从本文目录3.4知道聚合的设计原则:在边界之外使用最终一致性。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的最终一致性。

领域事件驱动设计可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不必要求强一致性,而是基于事件的最终一致性。

领域事件的执行需要一系列的组件和技术来支撑。我们来看一下这个领域事件总体技术架构图,领域事件处理包括:事件构建和发布、事件数据持久化、事件总线、消息中间件、事件接收和处理等。

为了解耦微服务,微服务之间数据采用最终一致性原则。由于发布方是在消息总线发布消息以后,并不关心数据是否送达,或者送达后订阅方是否正常处理,因此有些技术人会担心发布方和订阅方数据一致性的问题。

虽然MQ有消息确认机制,但也不能100%保证数据不会出错,在对数据一致性要求比较高的业务场景中,要求发送方和订阅方的事件数据都必须落库,发送方除了保存业务数据以外,在往消息中间件发布消息之前,会先将要发布的消息写入本地库。而接收方在处理消息之前,需要先将收到的消息写入本地库。然后可以采用定期对发布方和订阅方的事件数据对账的操作,识别出不一致的数据。如果数据出现异常或不一致的情况,可以启动定时程序再次发送,必要时可以转人工操作处理。

另外,如果能接受分布式事务比较耗性能,对数据一致性要求又比较高,那可以考虑使用分布式事务框架,如seata,相关的内容后续有机会在介绍。

七、微服务边界

在 事件风暴 中,我们会梳理出业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出实体等领域对象。根据实体对象之间的业务关联性,将业务紧密相关的多个实体进行组合形成聚合,聚合之间是第一层边界。根据业务及语义边界等因素将一个或者多个聚合划定在一个限界上下文内,形成领域模型,限界上下文之间的边界是第二层边界。

为了方便理解,我们把这些边界分为:逻辑边界、 物理边界、 代码边界 。

1、逻辑边界

事件风暴对不同实体对象进行关联和聚类分析后,会产生多个聚合和限界上下文,它们一起组成这个领域的领域模型。微服务内聚合之间的边界就是逻辑边界。

逻辑边界在微服务设计和架构演进中具有非常重要的意义!

微服务架构演进时,在业务端以聚合为单位进行业务能力的重组,在微服务端以聚合的代码目录为单位进行微服务代码的重组。由于按照 DDD 方法设计的微服务逻辑边界清晰,业务高内聚,聚合之间代码松耦合,因此在领域模型和微服务代码重构时,我们就不需要花费太多的时间和精力了。

2、物理边界

物理边界主要从部署和运行的视角来定义微服务之间的边界。不同微服务部署位置和运行环境是相互物理隔离的,分别运行在不同的进程中。这种边界就是微服务之间的物理边界。

3、代码边界

代码边界主要用于微服务内的不同职能代码之间的隔离。微服务开发过程中会根据代码模型建立相应的代码目录,实现不同功能代码的隔离。由于领域模型与代码模型的映射关系,代码边界直接体现出业务边界。代码边界可以控制代码重组的影响范围,避免业务和服务之间的相互影响。微服务如果需要进行功能重组,只需要以聚合代码为单位进行重组就可以了。

继续阅读