领域和子域
领域就是用来确定边界的。DDD会按照一定规则对业务领域进行细分,将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。DDD的领域就是这个边界内要解决业务问题域。领域越大,业务边界范围越大,反之则相反。领域可以进一步划分子域,每个子域对应一个更小的问题域,或者更小的业务范围。
领域会被细分为不同的子域,可以根据子域的重要性和功能属性划分为三类子域:核心子域、通用子域和支撑子域。技术团队一般会将核心子域的建设排在首位,对核心子域的建设,最好要有绝对的掌控能力和自主研发能力。如果资源实在有限,在支撑子域或者通用子域建设时,可以采用外购方式。通用子域建模,要考虑领域模型的抽象和标准化,以便实现企业级复用。
限界上下文
通用语言
通用语言,定义上下文对象的含义。通用语言,是团队的 统一语言,是在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确地描述业务含义和规则的语言。
DDD领域分析和设计过程中,我们可以用表格记录事件风暴和微服务设计过程中产生的领域对象以及其属性,如:领域对象在DDD分层架构中的位置、实体的属性、领域对象之间的依赖关系以及代码对象与领域对象的映射关系等。举个栗子:
限界上下文
限界上下文:定义领域边界,确保每个上下文对象在它特定的边界内具有唯一的含义,在这个边界内,组合这些对象构建领域模型。定义限界上下文要考虑领域业务职责单一,将与领域无关的对象统一排除在外。
领域可以拆分为多个子领域。一个领域相当于一个问题域,领域拆分为子域的过程就是大问题拆分为小问题的过程。子域还可以进一步拆分为子子域,拆分到一定程度后,有些子子域的领域边界就可能变成界限上下文的边界。如果子域和限界上下文边界刚好一致,那就是一对一的关系,而如果一个子域内还可以划分多个限界上下文,那就是一对多的关系。
微服务拆分,可以参考限界上下文的业务领域边界。但是 “不宜过度拆分微服务” ,这个会增加集成和运维成本。
实体和值对象
实体和值对象是组成领域模型的基础单元。
实体,是拥有唯一标识符的对象。对于这些对象,重要的不是属性,而是其延续性和标识。
在代码模型中,实体的表现形式是实体类(DO),它包含实体的属性和方法,通过方法实现自身业务行为和业务逻辑。DDD强调面向对象的设计方法。这些实体通常采用充血模型,与实体相关的所有业务逻辑都在实体类中实现,跨多个实体的领域逻辑则在领域服务中实现。
在数据库设计中,DDD是先构建领域模型,通过场景分析找出实体对象和行为,再将实体对象映射成数据持久化对象。在领域模型映射到数据库模型时,一个实体可以对应0个、1个或者多个数据库持久化对象。
- 大多数情况下实体域持久化 对象时一对一关系
- 一对零场景,有些实体只是暂驻内存的运行态,不需要持久化。如:加个配置数据计算后生成的折扣实体
- 一对多场景,如权限实体(DO)对应用户USER、角色ROLE两个持久化对象
- 多对一场景,如为了避免数据库连表查询,可以将客户信息CUSTOMER、账户信息ACCOUNT两类数据保存到同一张数据库表中。客户、账户两个实体对应一个持久化对象。
值对象,是通过对象属性值来识别的对象。它将多个相关属性组合为一个概念整体,具有不可修改的属性,并且是没有标识符的对象。简单说,值对象本质是一个属性集合。很多值对象的数据可能来源于其他聚合,它们以数据冗余的方式完成不同领域中数据的流转和共享。如下图的地址值对象示例:
DDD引入值对象是希望实现从“数据建模为中心”向“领域建模为中心”的转变,减少数据库表的数量和表与表之间复杂的依赖关系,尽可能简化数据库设计,提升数据库性能。要如何设计值对象?
1. 属性嵌入的方法
优势:提升了数据库的性能
局限:实体引用的值对象过多,则会导致实体堆积一堆缺乏概念完整性的属性
案例 :以属性嵌入的方式形成的人员实体对象,地址值对象直接以属性值嵌入人员实体中。
2. 序列化大对象的方法
优势:简化了数据库设计,减少了实体表的数量,可以简单、清晰地表达业务概念。
局限:无法满足基于值对象的快速查询,会导致搜索值对象属性值变得异常困难。(MySQL7.8开始支持JSON串查询)
案例 :以序列化大对象的方式形成的人员实体对象,地址值对象被序列化成大对象 Json串后,嵌入人员实体中。
聚合和聚合根
聚合,是能让实体和值对象协同工作的组织。
在DDD中,实体一般对应业务对象,具有丰富的业务属性和业务行为。值对象主要是属性集合,主要完成对实体的状态和特征描述。领域模型内的实体和值对象就类似这些组织中的个体,而能让实体和值对象协同工作的组织就是聚合。聚合内数据修改必须由聚合根统一组织,以确保每次数据修改都是按照聚合内统一的业务规则来完成的,聚合是数据修改和持久化的基本单元。
聚合在DDD分层架构里属于领域层,同一个微服务的领域层可以有多个聚合,每个聚合内有一个聚合根,多个实体、值对象和领域服务等来实现的。聚合在领域模型里是一个逻辑边界,聚合的业务逻辑是由聚合内的聚合根、实体、值对象和领域服务等来实现的。聚合内的实体以充血模型实现自身业务逻辑,跨多个实体的领域逻辑通过领域服务来实现。跨多个聚合的业务逻辑组合和编排,通过应用服务来实现。
聚合根,则是聚合这个组织的负责人
聚合根也称根实体,但它不仅是实体,还是聚合的管理者。聚合内由一定的业务规则以确保聚合内数据的一致性。聚合根的主要目的是避免聚合内由于复杂数据模型缺少统一的业务规则控制,而导致聚合内实体和值对象等领域对象之间数据不一致的问题。
大部分富领域模型的业务领域,都能找到聚合根,建立聚合,划分限界上下文,建立领域模型。但也有部分贫领域模型的场景,如数据计算、统计以及批处理等业务场景,这些实体都是平等、独立且无依赖的,如果找不到聚合根,而实体对象的业务又是高内聚的,也可以将这些关联紧密的业务实体作为一个没有聚合根的聚合处理。
如何判断一个实体是否是聚合根?
- 是否有独立的生命周期?
- 是否有全局唯一ID?
- 是否可以创建或修改其他对象?
- 是否有专门的模块来管理这个实体?
聚合设计步骤
以保险投保业务场景为例,分析一下聚合构建过程中又哪些关键步骤。如图:
步骤 | 说明 |
---|---|
1 | 采用事件风暴,通过场景分析,梳理出其中的实体和值对象。如账户,客户,投保单等 |
2 | 找出适合作为聚合对象管理者的根实体(聚合根)。如投保,客户 |
3 | 按照业务的职责单一和高内聚原则,找出聚合根紧密关联的实体和值对象,构建以聚合根为中心的集合,即得到聚合。如投保聚合,客户聚合 |
4 | 在聚合内部找出聚合根、实体和值对象的引用和依赖关系。如客户依赖地址和账户 |
5 | 多个聚合根据业务语义和上下文边界,划分在同一个限界上下文内 |
注:投保人和被保人零个值对象数据是客户数据的冗余数据,记录投保那一刻投保人和被保人的客户快照数据。如果希望得到他们的最新数据,可以通过关联客户ID从客户聚合中查询后获取。
聚合的设计原则
原则 | 说明 |
---|---|
有一套不变的业务规则 | 聚合内的实体和值对象,按照统一的业务规则运行,保证对象数据的一致性。边界外的任务东西都跟该聚合无关。 |
设计小聚合 | 小聚合设计降低由于业务过大,在业务变化是导致聚合重构的可能性,更能适应业务变化。 |
通过唯一标识引用其他聚合 | 聚合间通过引用聚合根ID的方式,而不是直接通过对象引用的方式。聚合边界清晰,同时在微服务拆分时,减少了重构跨服务调用的代码量。 |
边界之外使用最终一致性 | DDD强调一次事务中,最多只能修改一个聚合数据。涉及多个聚合数据的修改,应采用领域事件驱动机制,通过数据最终一致性异步更新所有聚合数据,实现聚合解耦。 |
通过应用层实现跨聚合的服务调用 | 避免领域层聚合间发送耦合,将聚合间的服务调用上升到应用层。 |
聚合、聚合根、实体和值对象关系
对象 | 说明 |
---|---|
聚合的特点 | 聚合内部业务逻辑高内聚,聚合之间低耦合,聚合是领域模型中最小的业务逻辑边界。 |
聚合根的特点 | 聚合根是实体,有实体的特点,拥有全局唯一标识,有独立的生命周期。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象通过对象引用的方式进行组织和协调,聚合与聚合之间只能通过聚合根ID引用的方式,实现聚合之间的访问和协同。 |
实体的特点 | 实体有ID标识,通过ID判断相等性,ID在聚合内唯一即可。实体的状态可变,它依附于聚合根,其生命周期由聚合根管理。实体一般会持久化,但持久化对像不一定是一对一的关系。实体可以引用聚合内的聚合根、实体和值对象。 |
值对象的特点 | 值对象无ID,且数据不可变,他没有生命周期,用完即丢。值对象通过属性值判断相等性。它是一组概念完整的属性组成的集合,用于描述实体的状态和特征,其核心本质是“值”。 |
领域事件
领域事件采用事件驱动架构(EDA)设计,可以切断领域模型之间的强依赖,事件发布方不必关心订阅方的事件处理是否成功,从而实现领域模型的解耦。
领域事件一般都会结合消息中间件和事件发布订阅的异步处理方式,实现数据最终一致性。对于实时性和数据一致性要求高的业务场景,可以在应用服务中增加事务控制,实现多聚合提交数据时的强一致性,但是会对系统性能造成损耗。
领域事件驱动实现机制
领域事件处理包括:领域事件构建和发布、领域事件数据持久化、事件总线、消息中间件、事件接收和处理等。总体技术架构如下图所示:
1. 事件构建和发布
事件实体基本属性至少要包含:事件唯一标识、发生时间、事件类型和事件来源、业务数据。为了统一领域事件类数据结构,我们设计一个DomainEvent。如图:
对于关键的不允许丢失和需要对账的事件数据,在事件发布之前需要先持久化到数据库事件表中。事件发布可以在应用服务或领域服务中完成,将领域事件数据发布到事件总线(微服务内)或者消息中间件(微服务之间);也可以采用定时程序或数据库日志捕获技术,从数据库事件表中获取增量事件数据,发布到消息中间件。
2. 事件数据持久化
事件数据持久化有两个方案:
- 持久化到本地业务数据库的事件表中,利用本地事务保证业务和时间数据的一致性。
- 持久化到共享的事件数据库中。由于业务和事件数据不在同一个数据库中,因此持久化会跨数据库,需要分布式事务机制来保证数据的强一致性。
3. 事件总线
事件总线时实现微服务内聚合之间领域事件传输的重要技术组件,提供事件分发和接收等服务。可以通过事件总线配置,选择同步或异步模式传递数据。
事件总线分发流程:
- 微服务内的订阅者(其他聚合),则直接分发到指定订阅者。
- 微服务外的订阅者,首先将事件数据保存到事件库(表),然后异步发送到消息中间件。
- 如果存在微服务内和微服务外订阅则,则先分发到内部订阅者,再将事件数据保存到事件库(表),在异步发送到消息中间件。
4. 消息中间件
消息中间间主要有两种消息发布机制:应用逻辑推送和数据库数据增量推送。
- 应用逻辑推送机制,通过应用实现逻辑完成业务数据和事件数据持久化,在应用逻辑中直接将源端事件数据推送至消息中间件,完成事件发布。需要引入十五机制,保证业务逻辑和消息发布的数据强一致性。
- 数据库数据增量推送机制,是在事件数据完成持久化后,通过数据库日志捕获技术(CDC)获取事件增量数据,并将事件增量数据推送到消息中间件,完成事件发布。
5. 事件接收和处理
订阅者在进行事件数据持久化时,可以以事件实体的ID为主键,通过主键约束规避对事件数据的重复消费。完成事件数据持久化后,开始进一步的业务处理,完成业务处理后,修改持久化事件表中的事件状态数据,同时把处理结果推送至消息中间件的反馈队列,反馈结果给发布者。
领域事件订阅逻辑在应用层实现,领域事件的业务处理逻辑在领域层的领域服务中实现。