本文为极客时间《DDD实战》的读书笔记
DDD-基础
DDD基础介绍
微服务设计和拆分的困境
对于微服务的拆分粒度很模糊,不知道业务或者微服务的边界是什么,没有一套方法论来支持微服务的拆分。
DDD适合微服务的原因
DDD 核心思想是通过<font color=red>领域驱动</font>设计方法定义<font color=red>领域模型</font>,从而确定<font color=red>业务</font>和<font color=red>应用边界</font>,保证<font color=red>业务模型与代码模型的一致性</font>,DDD是一种<font color=red>架构设计的方法论</font>,它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现架构演进。
DDD的战略设计和战术设计
-
战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
-
战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
建立领域模型的方法
DDD 战略设计会建立领域模型,领域模型可以用于指导微服务的设计和拆分。
事件风暴
事件风暴是建立领域模型的主要方法,它是一个从发散到收敛的过程。
-
发散
- 它通常<font color=red>采用用例分析、场景分析和用户旅程分析</font>,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。
-
收敛
- 事件风暴过程会产生很多的实体、命令、事件等领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程。
三步划定领域模型和微服务的边界
-
在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。
-
根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在下图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。
-
根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。在这个图里,限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。
DDD和微服务的关系
DDD 是一种<font color=red>架构设计</font>方法,微服务是一种<font color=red>架构风格</font>,两者从本质上都是为了追求高响应力,而从业务视角去分离应用系统建设复杂度的手段。两者都强调从业务出发,其核心要义是强调根据业务发展,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力,也就是我们常说的演进式架构。\
-
DDD 主要关注:
-
从业务领域视角划分领域边界,构建通用语言进行高效沟通;
-
通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性;
-
-
微服务主要关注:
-
运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理;
-
关注微服务的独立开发、测试、构建和部署;
-
DDD架构设计方法带来的好处
-
DDD 是一套完整而系统的设计方法,它能带给你从战略设计到战术设计的标准设计过程,使得你的设计思路能够更加清晰,设计过程更加规范。
-
DDD 善于处理与领域相关的拥有高复杂度业务的产品开发,通过它可以建立一个核心而稳定的领域模型,有利于领域知识的传递与传承。
-
DDD 强调团队与领域专家的合作,能够帮助你的团队建立一个沟通良好的氛围,构建一致的架构体系。
-
DDD 的设计思想、原则与模式有助于提高你的架构设计能力。
-
无论是在新项目中设计微服务,还是将系统从单体架构演进到微服务,都可以遵循 DDD 的架构原则。
-
DDD 不仅适用于微服务,也适用于传统的单体应用。
DDD的领域、子域、核心域、通用域和支撑域
领域
领域是用来确定范围的,领域等同于范围,范围即边界,这也是DDD在设计中不断强调边界的原因。
DDD的领域就是这个边界内要解决的<font color=red>业务问题域</font>。
子域
我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。
在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。
核心域
决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。<font color=red>子域的核心业务</font>。
通用域
没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。<font color=red>多个子域共用的业务</font>。
支撑域
既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。<font color=red>基础支撑层,主要是一些功能性的代码实现,如工具类等</font>。
限界上下文:定义领域边界的利器
通用语言定义上下文含义,限界上下文则定义领域边界。
通用语言
在事件风暴过程中,通过团队交流达成共识的,能够<font color=red>简单、清晰、准确描述业务涵义和规则</font>的语言就是通用语言。也就是说,通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。
通用语言包含<font color=red>术语</font>和<font color=red>用例场景</font>,并且能够直接反映在代码中。
-
通用语言中的名词可以给领域对象命名,如商品、订单等,对应实体对象;
-
而动词则表示一个动作或事件,如商品已下单、订单已付款等,对应领域事件或者命令。
从事件风暴建立通用语言到领域对象设计和代码落地的完整过程:
image.png-
在事件风暴的过程中,领域专家会和设计、开发人员一起建立领域模型,在领域建模的过程中会形成<font color=red>通用的业务术语</font>和<font color=red>用户故事</font>。事件风暴也是一个项目团队统一语言的过程。
-
通过用户故事分析会形成一个个的领域对象,这些<font color=red>领域对象</font>对应领域模型的<font color=red>业务对象</font>,<font color=red>每一个业务对象和领域对象都有通用的名词术语,并且一一映射</font>。
-
微服务代码模型来源于领域模型,每个代码模型的代码对象跟领域对象一一对应。
作者的经验分享
设计过程中我们可以用一些表格,来记录事件风暴和微服务设计过程中产生的领域对象及其属性
image.pngDDD 分析和设计过程中的每一个环节都需要保证限界上下文内术语的统一,在代码模型设计的时侯就要建立领域对象和代码对象的一一映射,从而保证业务模型和代码模型的一致,实现业务语言与代码语言的统一。
限界上下文
我们知道语言都有它的语义环境,同样,通用语言也有它的上下文环境。为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD 在战略设计上提出了“限界上下文”这个概念,用来<font color=red>确定语义所在的领域边界</font>。
我们可以将限界上下文拆解为两个词:限界和上下文。
-
限界就是领域的边界
-
上下文则是语义环境。
通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流。
作者认为限界上下文的定义
用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。
例子:
例子一:
在一个明媚的早晨,孩子起床问妈妈:“今天应该穿几件衣服呀?”妈妈回答:“能穿多少就穿多少!”那到底是穿多还是穿少呢?如果没有具体的语义环境,还真不太好理解。但是,如果你已经知道了这句话的语义环境,比如是寒冬腊月或者是炎炎夏日,那理解这句话的涵义就会很容易了。所以语言离不开它的语义环境。而业务的通用语言就有它的业务边界,我们不大可能用一个简单的术语没有歧义地去描述一个复杂的业务领域。限界上下文就是用来细分领域,从而定义通用语言所在的边界。
例子二:
现在我们用一个保险领域的例子来说明下术语的边界。保险业务领域有投保单、保单、批单、赔案等保险术语,它们分别应用于保险的不同业务流程。
-
客户投保时,业务人员记录投保信息,系统对应有投保单实体对象。
-
缴费完成后,业务人员将投保单转为保单,系统对应有保单实体对象,保单实体与投保单实体关联。
-
如客户需要修改保单信息,保单变为批单,系统对应有批单实体对象,批单实体与保单实体关联。
-
如果客户发生理赔,生成赔案,系统对应有报案实体对象,报案实体对象与保单或者批单实体关联。
投保单、保单、批单、赔案等,这些术语虽然都跟保单有关,但不能将保单这个术语作用在保险全业务领域。因为术语有它的边界,超出了边界理解上就会出现问题。
个人理解限界上下文的作用:
-
1.确定通用语言的适用范围和语义;
-
2.限定领域模型中的业务对象的适用范围;
-
3.领域边界就是通过限界上下文来定义的;
限界上下文和微服务的关系
保险领域还是很复杂的,在这里我用一个简化的保险模型来说明下限界上下文和微服务的关系。
图4
-
首先,领域可以拆分为多个子领域。一个领域相当于一个问题域,领域拆分为子域的过程就是大问题拆分为小问题的过程。在这个图里面保险领域被拆分为:投保、支付、保单管理和理赔四个子域。
-
子域还可根据需要进一步拆分为子子域,比如,支付子域可继续拆分为收款和付款子子域。拆到一定程度后,有些子子域的领域边界就可能变成限界上下文的边界了。
-
子域可能会包含多个限界上下文,如理赔子域就包括报案、查勘和定损等多个限界上下文(限界上下文与理赔的子子域领域边界重合)。也有可能子域本身的边界就是限界上下文边界,如投保子域。
-
每个领域模型都有它对应的限界上下文,团队在限界上下文内用通用语言交流。领域内所有限界上下文的领域模型构成整个领域的领域模型。
理论上限界上下文就是微服务的边界。我们<font color=red>将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案</font>。可以说,限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。
实体和值对象:从领域模型的基础单元看系统设计
实体
在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其<font color=red>延续性</font>和<font color=red>标识</font>,对象的延续性和标识会<font color=red>跨越甚至超出软件的生命周期</font>。我们把这样的对象称为实体。
<font color=red>实体是由属性构成,其有唯一性,即拥有唯一性的属性,比如id</font>。
业务形态
实体是领域模型的一个业务对象的体现,定义了一类业务对象,该业务对象对应一类业务,自创建起其属性,除了唯一标识属性不可改变,剩下的属性都可以改变,无论非唯一属性如何改变,他依旧代表他被创建时的那个业务对象,也就是其最大的特点,唯一性和延续性。
比如创建出一个用户,无论他的年龄,身份,职业如何改变,但是他的id不变,他依旧是那个用户。
代码体现
一个拥有唯一标识的DO对象,domain。
运行的形态
拥有唯一标识的domain,除了唯一标识的属性不可改变,其他属性理论上都可以改变。
实体的数据库形态
实体对应的是一类业务对象,并不和数据库表直接对应,持久层对象(dao/repository)直接对应数据库表。一个业务对象可以解析成多个持久层对象。
值对象
实体中的一些属性可以按照<font color=red>一定的业务逻辑进行聚合成一个集合(具体体现为对象或者一个json)</font>。<font color=red>其没有唯一性,但是其自创建起不能被修改</font>,其由属性或者json组成,其中的属性不能被修改,只能被完全替换。
《实现领域驱动设计》一书中对值对象的定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。
<font color=pink>例子</font>
image.png人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。这样显示地址相关的属性就很零碎了对不对?现在,我们可以将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,这个集合就是值对象了。
值对象的业务形态
其按照一定的业务逻辑对一些相关属性进行聚合。
值对象的代码形态
可能是一个类
image.png也可能是一个json
image.png值对象的运行形态
值对象没有唯一标志,其被创建后,不允许对其中属性的更改,只能整体替换。
值对象按照嵌入实体的方式有两种实现:
1.将一些实体中的属性聚合成类。
2.序列化大对象,将值对象序列化成json格式,存储在某一个属性上。
嵌入方式
值对象嵌入到实体的话,有这样两种不同的数据格式,也可以说是两种方式,分别是属性嵌入的方式和序列化大对象的方式。
案例 1:以属性嵌入的方式形成的人员实体对象,地址值对象直接以属性值嵌入人员实体中。
image.png案例 2:以序列化大对象的方式形成的人员实体对象,地址值对象被序列化成大对象 Json 串后,嵌入人员实体中。
image.png值对象的数据库形态
值对象最大的特点体现就在这,其并不对应某一个表,只对应表中的某几个字段或者其中的一个字段。
在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。
领域驱动中实体和值对象与数据库驱动的区别
领域驱动先设计领域,后设计表,弱化了表在业务中的体现,通过设计领域对象,解耦业务和表之间的直接关系。简化数据库设计,不用像以前数据库驱动设计,一个实体对应一个表,减少了实体表的数量,可以简单、清晰地表达业务概念。这种设计方式虽然降低了数据库设计的复杂度,
值对象的优点和局限性
优点
通过值对象对一个实体中某一些属性进行聚合,可以直接简化数据库的设计,减少数据库表的数量,弱化数据库在业务中的耦合性,弱化数据库的作用,只把数据库作为一个保存数据的仓库即可。
局限性
值对象采用序列化大对象的方法简化了数据库设计,减少了实体表的数量,可以简单、清晰地表达业务概念。这种设计方式虽然降低了数据库设计的复杂度,但却无法满足基于值对象的快速查询,会导致搜索值对象属性值变得异常困难。
实体和值对象的关系
实体是业务中具体的业务对象的体现,而值对象是业务中对象中有一定相关性属性的一个集合。
实体有唯一性和延续性。
值对象没有唯一性,其属性有不可变性,若要改变值对象,必须完全替换值对象,不能修改其中具体属性。
聚合和聚合根
聚合
领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。
你可以这么理解,聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
个人理解:
聚合:
按照一个功能/模块/业务对项目进行划分,将符合同一个功能/模块/业务的实体,值对象找出来,聚合到同一个领域内。
聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的。
聚合属于战略设计上的一个概念,不直接对应代码实现,聚合在代码中的体现是聚合根。
聚合根
负责按照聚合的逻辑,将符合同一个业务逻辑的Entity,VO,聚合到一个class中。
如何设计聚合
image.png第 1 步:
采用事件风暴,根据业务行为,梳理出在投保过程中发生这些行为的所有的实体和值对象,比如投保单、标的、客户、被保人等等。
第 2 步:
从众多实体中选出适合作为对象管理者的根实体,也就是聚合根。判断一个实体是否是聚合根,你可以结合以下场景分析:是否有独立的生命周期?是否有全局唯一 ID?是否可以创建或修改其它对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体。
第 3 步:
根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出 1 个包含聚合根(唯一)、多个实体和值对象的对象集合,这个集合就是聚合。在图中我们构建了客户和投保这两个聚合。
第 4 步:
在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。这里我需要说明一下:投保人和被保人的数据,是通过关联客户 ID 从客户聚合中获取的,在投保聚合里它们是投保单的值对象,这些值对象的数据是客户的冗余数据,即使未来客户聚合的数据发生了变更,也不会影响投保单的值对象数据。从图中我们还可以看出实体之间的引用关系,比如在投保聚合里投保单聚合根引用了报价单实体,报价单实体则引用了报价规则子实体。
第 5 步:
多个聚合根据业务语义和上下文一起划分到同一个限界上下文内。这就是一个聚合诞生的完整过程了。
聚合的一些设计原则
《实现领域驱动设计》一书中对聚合设计原则的描述,原文是有点不太好理解的,我来给你解释一下。
1. 在一致性边界内建模真正的不变条件。
<font color=red>聚合用来封装真正的不变性</font>,而不是简单地将对象组合在一起。
- 聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。
<font color=apple green>个人理解: 聚合实际上是聚合了一类业务逻辑</font>
- 聚合内的业务规则一般不变,实体和值对象都遵从于该业务逻辑。任何外界变化不会影响到聚合,以实现聚合的高聚合特性。
2. 设计小聚合
- 如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。
<font color=apple green>个人理解: 小聚合</font>
- 尽量简单化聚合,防止聚合过于庞大,聚合越小,业务重构的成本越低。
3. 通过唯一标识引用其它聚合
- 聚合之间是通过关联外部聚合根 ID 的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。
<font color=apple green>个人理解: 防止聚合之间直接引用,而是通过唯一标识引用其他聚合</font>
- 如果直接引用其他聚合对象,会增加耦合度,通过唯一标识聚合其他聚合根。
4. 在边界之外使用最终一致性
- 聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦。
<font color=apple green>个人理解: 同一个聚合内数据强一致性,聚合之间数据最终一致性。</font>
5. 通过应用层实现跨聚合的服务调用
- 为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联
<font color=apple green>个人理解: 通过应用层实现跨聚合的服务调用</font>
- 对于跨聚合之间的调用,放在应用层中,在聚合之间再加一层。
上面的这些原则是 DDD 的一些通用的设计原则,还是那句话:“适合自己的才是最好的。”在系统设计过程时,你一定要考虑项目的具体情况,如果面临使用的便利性、高性能要求、技术能力缺失和全局事务管理等影响因素,这些原则也并不是不能突破的,总之一切以解决实际问题为出发点。
网友评论