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

实体和值对象,都是领域模型中的领域对象。

实体:

实体拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一直。对于实体,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。

业务形态:领域模型中的实体是多个属性、操作或行为的载体。实体和值对象是组成领域模型的基础单元。

代码形态:实体类,包含了实体的属性和方法。DDD中实体类通常采用充血模型。

运行形态:实体以DO(领域对象)的形式存在,每个实体都有唯一的ID。对实体进行多次修改,实体仍然有相同的ID,释然是同一个实体。

数据库形态:DDD先构建领域模型,根据场景构建实体和行为,再将实体映射到数据持久化对象。一个实体可能对应0、1或对各数据库持久化对象,大多数情况下是一对一。用户user与角色role两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。

 

值对象:

通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在DDD中是一个没有标识符的对象。

可以将值对象理解为属性的集合。

DDD学习笔记(Ⅱ)

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

业务形态:值对象和实体一起构成聚合。实体是看得到、摸得到实实在在的业务对象,具有业务属性、业务行为和业务逻辑。值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集用于描述实体的特征。

代码形态:两种。如果值对象是单一属性,则直接定义为实体类的属性;如果值对象是属性集合,则设计为Class类,Class将具有整体概念的多个属性归集到属性集合,这样的值对象没有ID,会被实体整体引用。

看下代码,person实体有若干个单一属性的值对象,比如id、name等属性;同时也包含多个属性的值对象,如地址addrress:
DDD学习笔记(Ⅱ)

运行形态:实体实例化后的DO对象的业务属性和行为很丰富,但值对象除了数据初始化和整体替换意外,其他业务行为就很少了

值对象嵌入到实体,有两种方式。属性嵌入和序列化大对象。引用单一属性的值对象或只有一条记录的多属性值对象的实体,可以采用属性嵌入的方式。引用一条或多条记录的多属性值对象的实体,可以采用序列化大对象的方式。值对象创建后就不能修改了,只能用另一个值对象整体替换。

案例1:艺术性嵌入的方式形成的人员实体对象,地址值对象直接以属性值嵌入人员实体中。

DDD学习笔记(Ⅱ)

案例2:一序列化大对象的方式形成的人员实体对象,地址值对象被序列化成大对象Json,嵌入人员实体中。

DDD学习笔记(Ⅱ)

数据库形态:DDD引入值对象是希望实现从“数据建模为中心”向“领域建模为中心”转变,减少数据库表数量和复杂的依赖关系。传统的数据建模大多是根据数据库范式设计的,每一个数据库表对应一个实体,每一个实体的属性值用单独的一列来存储,一个实体主表会对应N个实体从表。而值对象在数据库持久化方面简化了设计,它的数据库设计大多采用非数据库范式,值对象的属性值和实体的属性值保存在同一个数据库实体表中。

例如上文的人员和地址的场景,实体和数据模型设计有两种方式:第一是把值对象的所有属性都放到人员实体表中,创建人员实体,创建人员表;第二是创建人员和地址两个实体,同时创建人员和地址两张表。第一个方案会破坏地址的业务含义和概念完整性,第二个方案增加了不必要的实体和表,需要处理多个表关系,增加了数据库复杂性。

所以要在这两者之间权衡。可以综合优势。

领域建模时,可以把地址作为值对象,人员作为实体,保留了业务含义和概念完整性。而在数据建模时,可以将地址的属性值嵌入到人员实体数据库表中,只创建人员数据库表。这样兼顾了业务含义,也不增加数据库复杂度。

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

也有DDD专家认为,需要有限领域建模,弱化数据库的作用,只作为一个保存数据的仓库即可。

优势和劣势:简化数据库设计,提升数据库性能。减少了实体表的数量,简单、清晰的表达业务概念。但却无法满足基于值对象的快速查询,导致搜索值对象书香之变得困难。如果实体引用的值对象过多,会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就是去了业务含义,操作也不方便。

 

实体和值对象的关系:

实体和值对象时微服务底层最基础的对象。在某些场景下时可以互换的,值对象在某些场景下有很好的价值,但并不是所有场景都适合值对象。

DDD引入值对象还有一个重要的原因,就是到底领域建模优先还是数据建模优先?

DDD提倡从领域模型设计出发,而不是有限设计数据模型。值对象的诞生,在一定程度上和实体是互补的。

在某些场景中,地址会被某一实体引用,它只承担描述实体的作用,并且它的值只能被整体替换,这时候就可以将地址设计为值对象,比如收货地址。而在其他业务场景中,地址会被经常修改,是作为一个独立对象存在的,这时候应该将它设计为实体,比如行政区划中的之地信息维护。

 

实体着重唯一性和延续性,不在意属性的变化,属性全变了,它还是原来那个它;值对象着重描述性,对属性的变化很敏感,属性变了,它就不是那个它了。

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

 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

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

将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文中,并在限界上下文内完成领域建模。

聚合:DDD中实体一般对应业务对象,具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都是个体化的对象,他们的行为表现出来的是个体的能力。

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

就好像一个个人和社团、机构、部门等组织的关系。

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

聚合在DDD分层架构中属于领域层,内部实体以充血模型实现个体业务能力。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。

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

如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也成为根实体,它不仅是实体,还是聚合的管理者。作为实体,拥有实体的属性和业务行为,实现自身业务逻辑。作为聚合管理者,负责协调实体和值对象按照固定业务规则协同完成业务逻辑。在聚合之间,它还是聚合对外的接口人,以聚合根ID关联的方式接受外部任务的请求,在上下文内实现聚合之间的业务协同。即聚合之间通过聚合根ID关联引用。访问其他聚合的实体,需要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内的实体。

怎样设计聚合?

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

以保险的投保业务场景为例:

DDD学习笔记(Ⅱ)

  • 第1步:事件风暴,根据业务行为,梳理出在投保过程发生这些行为的所有的实体和值对象,比如投保单、标的、客户、被保人等;
  • 第2步:从众多实体中选出适合作为对象管理者的根实体,也就是聚合根。可以根据以下场景分析:是否有独立的生命周期?是否有全局唯一ID?是否可以创建或修改其他对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体;
  • 第3步:根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密以来的实体和值对象。构建出1个包含聚合根(唯一)、多个实体和值对象的对象集合,这个集合就是聚合。图中构建了客户和投保这两个聚合;
  • 第4步:在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。上图中投保人和被保人是通过关联客户ID从客户聚合中获取的,在投保聚合中他们是投保单的值对象,是客户的冗余数据,即使未来客户聚合的数据发生了改变,也不影响投保单的值对象数据。可以看出实体之间的引用关系,比如在投保聚合里投保单集合跟引用了报价单实体,报价单实体则引用了报价规则子实体。
  • 多个聚合根据业务语义和上下文一起划分到同一个限界上下文中。

聚合的设计原则:

1.在一致性边界内建模真正的不变条件。聚合用来封装真正的不变性,而不是简单地将对象组合在一起。边界外的东西都与该聚合无关,这也是聚合能实现业务高内聚的原因。

2.设计小聚合。如果聚合过大,包含的实体过多,实体之间的管理就会很复杂,高频操作会出现并发冲突或数据库锁,导致可用性变差。

3.通过唯一标识引用其他聚合。聚合之间通过关联外部聚合根ID的方式引用,而不是直接对象引用。边界清晰,不会增加耦合度

4.在边界之外使用最终一致性。聚合内数据强一致性,聚合之间数据最终一致性。一次事务最多只能更改一个聚合的状态。若一次业务操作设计多个聚合状态的修改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间解耦。

5.通过应用分层实现跨聚合的服务调用。应避免跨聚合的领域服务调用和跨聚合的数据库表关联。

 

聚合的特点:高内聚、低耦合。是领域模型中最底层的边界,可以作为拆分微服务的最小单位。一个微服务可以包含多个聚合,聚合之间的边界时微服务内天然的逻辑边界。

聚合根的特点:聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。一个聚合内只有一个聚合根,聚合根在聚合内对实体和值对象采用直接引用的方式进行组织和协调,聚合与聚合根之间通过ID关联的方式实现聚合之间的协同。

实体的特点:有ID标识,通过ID判断相等性,ID在聚合内唯一即可。状态可变,依附于聚合根,生命周期由聚合根管理。实体一般会持久化,但与数据库持久化对象不一定是一对一的关系。实体可以引用聚合内的聚合根、实体和值对象。

值对象的特点:无ID,不可变,无生命周期,用完即扔。值对象之间通过属性值判断相等性。它的核心本质是值,是一组概念外争的属性组成的集合,用于描述实体的状态的特征。值对象尽量只引用值对象。

 

 

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

 

06 | 领域事件:解耦微服务的关键

在事件风暴中,除了命令和操作等业务行为,还有领域事件,这种事件发生后通常会导致进一步的业务操作。

领域事件用来白哦是领域中发生的事件。在实现业务解耦的同时,还有助于形成完整的业务闭环。例如,领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作;也可能是定时批处理过程发生的事件,比如批处理生成季缴保费通知单,触发发送缴费邮件通知操作;或者一个事件发生后触发的后续动作,比如密码连续输错三次,触发锁定账户的动作。

如何识别领域事件?在做用户旅程或场景分析时,捕捉业务、需求人员或领域专家口中的关键词:“如果发生...则...”、“当做完...的时候,请通知...”、发生...时,则...“等。这些场景发生某种时间后会触发进一步操作,那么这个事件很可能时领域事件。

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

有的领域事件发生在微服务内的聚合之间,有的发生在微服务之间,还有两者皆有的场景,一般来说跨微服务的领域事件处理居多。

 

1.微服务内的领域事件

当领域事件发生在微服务内的聚合之间,领域事件发生后完成事件实体构建和事件数据持久化,发布方聚合将事件发布到事件总线,订阅方接收事件数据完成后续业务操作。微服务内大部分事件的结成,都发生在同一个进程内,进程自身可以很好的控制事务,因此不一定需要引入消息中间件。但一个事件如果同时更新多个聚合,就要考虑是否引入事件总线。但微服务内的事件总线,会增加开发难度。微服务内应用服务,可以通过跨聚合的服务编排和组合,以服务调用的方式完成跨聚合的访问,这种方式通常应用于实时性和数据一致性要求高的场景。这个过程会用到分布式事务,保证发布方和订阅方数据同时更新成功。

2.微服务之间的领域事件

跨微服务的领域事件会在不同的限界上下文或领域模型之间实现业务协作,主要为了实现微服务解耦,减轻微服务之间实时访问的压力。这种场景比较多,事件处理机制也更复杂。跨微服务的事件可以推动业务流程或者数据在不同的子域或微服务间直接流转。跨微服务的事件机制要总体考虑事件构建、发布和订阅、事件数据持久化、消息中间件,甚至事件数据持久化时还要考虑引入分布式事务。

微服务之间的访问也可以采用应用服务直接调用的方式,实现数据的服务的实施访问,弊端就是跨微服务的数据同时变更需要引入分布式事务,这样会影响系统性能,增加服务之间耦合,还是要避免使用分布式事务。

 

领域事件相关案例

保险承保业务。一个保单的生成,经历了很多子域、业务状态变更和跨微服务业务数据的传递。产生了很多领域事件,促成了保险业务数据、对象在不同微服务和子域之间的流转和角色转换。

DDD学习笔记(Ⅱ)

如何用领域事件驱动设计来驱动承保业务流程:

事件起点:客户购买保险 - 业务完成保单录入 - 生成投保单 - 启动缴费动作。

1.投保微服务生成缴费通知单,发布第一个事件:将缴费通知单数据发布到MQ。收款微服务订阅该MQ,完成缴费操作。缴费通知单已生成,领域事件结束。

2.收款微服务缴费完成后,发布第二个事件:缴费已完成,将缴费数据发布到MQ。投保微服务收到该MQ并确认缴费完成,完成投保单转保单的操作。缴费已完成,领域事件结束。

3.投保微服务在投保单转保单完成后,发布第三个事件:保单已生成,将保单数据发布MQ。保单微服务接受到该MQ,完成保单数据保存操作。保单已生成,领域事件结束。

4.后面还会发生一系列领域事件,并发的将保单数据通过MQ发送到佣金、收付费、和再保等微服务,完成后续所有业务流程。

总之,通过领域事件驱动的异步化机制,可以推动业务流程和数据在各个微服务之间流转,实现微服务解耦。

领域事件总体架构

领域事件的执行需要一系列组件和技术做支撑。领域事件处理包括:事件构建和发布、事件数据持久化、事件总线、消息中间件、事件接受和处理等。

DDD学习笔记(Ⅱ)

1.事件构建与发布

事件基本属性至少包括:事件唯一标识、发生时间、事件类型和事件源。事件唯一标识应该是全局唯一的,以便无歧义的在多个限界上下文中传递。事件基本属性记录事件滋生以及事件发生背景的数据。时间还有业务属性,用于记录事件发生时的业务数据,会随事件传输到订阅方。事件基本属性和业务属性一起构成事件实体,事件实体依赖聚合根。领域事件发生后,事件中的业务数据不再修改,因此业务数据可以以序列化值对象的形式保存,这样在MQ中也比较容易解析和获取。

DDD学习笔记(Ⅱ)

事件发布前要县构建事件实体并持久化。事件发布的方式:可以通过应用服务或领域服务发布到事件总线或者MQ,也可以从事件表中利用定时程序或数据库日志捕捉技术获取增量事件数据,发布到MQ。

2.事件数据持久化

可用于系统之间的数据队长,或实现发布方和订阅方事件数据的审计。当遇到MQ、订阅方宕机或网络中断,在问题解决后仍可继续后续业务流转,保证数据一致性。

持久化方案有两种:

  • 持久化到本地业务数据库中,利用本地事务保证业务和事件数据的一致性
  • 持久化到共享的事件数据库中。业务数据库和事件数据库不是一个,他们的数据持久化操作会跨数据库,因此需要分布式事务来保证业务和事件数据的强一致性。

3.事件总线(EventBus)

事件总线是实现微服务内聚合之间领域事件的重要组件,提供事件分发和接收等服务。是进程内模型,会在微服务内聚合之间遍历订阅者列表,采取同步或异步的模式传递数据。事件分发流程大致如下:

  • 如果是微服务内的订阅者(其他聚合),则直接分发到指定订阅者;
  • 如果是微服务外的订阅者,将事件数据保存到事件库并异步发送到消息中间件;
  • 如果同时存在微服务内和外订阅者,则先处理内部订阅者,再处理外部订阅者

4.消息中间件

跨微服务的领域事件大多会用到消息中间件,实现跨微服务的事件发布和订阅。Kafka,RabbitMQ等

5.事件接收和处理

微服务订阅方再应用层采用监听机制,接收MQ中的事件数据,完成持久化后,可以开始进一步的业务处理。

 

领域事件运行机制案例

承保业务流程的通知缴费通知单事件为例。发生再投保和收款微服务之间。领域事件是:缴费通知单已生成。下一步业务操作是:缴费。

DDD学习笔记(Ⅱ)

事件起点:出单员生成投保单,核保通过后,发起生成缴费通知单的操作。

1.投保微服务应用服务,调用聚合中的领域服务createPaymentNotice和createPaymentNoticeEvent,分别创建缴费通知单、缴费通知单事件。缴费通知单类PaymentNoticeEvent继承基类DomainEvent。

2.利用仓储服务持久化缴费通知单相关的业务和事件数据。为避免分布式事务,这些数据都持久化到本地投保微服务数据库中。

3.通过数据库日志捕获技术或定时程序,从数据库事件表中获取事件增量数据,发布到MQ。事件发布也可以通过应用服务或领域服务完成发布。

4.收款微服务再应用层从MQ订阅缴费通知单事件消息主题,监听并获取事件数据后,应用服务调用领域层的领域服务将事件数据持久化到本地数据库。

5.收款微服务调用领域岑的领域服务PayPremium,完成缴费。

6.事件结束。

提示:缴费完成后,后续还会产生很多新的领域事件。

 

领域事件驱动是很成熟的技术,很多分布式架构中有大量的使用。是DDD的重要概念,用领域事件驱动业务流转。

 

 

 

 

 

 

 

 

 

相关文章:

  • 2021-11-29
  • 2021-11-08
  • 2021-07-16
  • 2021-12-06
  • 2021-12-11
  • 2021-04-16
  • 2021-08-05
  • 2022-12-23
猜你喜欢
  • 2021-10-18
  • 2021-12-20
  • 2021-06-15
  • 2022-12-23
  • 2021-04-25
  • 2021-08-29
  • 2021-06-24
相关资源
相似解决方案