DDD
什么是DDD DDD 全称 Domain driven Design,即领域驱动设计,是一种软件设计方法。也就是说 DDD 是指导我们做软件工程设计的一种手段,它提供了用于切割工程模型的各类技巧,比如,领域、界限上下文、实体、值对象、聚合、工厂、仓储等。通过 DDD 的指导思想,可以在前期投入更多的时间,更合理的规划处
什么是DDD
DDD 全称 Domain-driven Design,即领域驱动设计,是一种软件设计方法。也就是说 DDD 是指导我们做软件工程设计的一种手段,它提供了用于切割工程模型的各类技巧,比如,领域、界限上下文、实体、值对象、聚合、工厂、仓储等。通过 DDD 的指导思想,可以在前期投入更多的时间,更合理的规划处可持续迭代的工程设计。
在 DDD 中有一套共识的工程两阶段设计手段:
- 战略设计:主要是为了应对复杂的业务需求,通过抽象、分治的过程,合理的拆分为独立的多个服务,然后分而治之。判断拆分是否合理的标准,则是在需求开发上线的时候,是否每次都大量操作多个微服务开发和上线。这样的战略设计是一种失败的微服务单体设计。所有少数几个人中等规模的单体应用,周围围绕着一个服务生态系统,这更有意义
- 战术设计:在这个阶段,主要讨论如何基于面向对象思维,运用领域模型来表达业务概念。通常在不做领域模型设计的架构,也就是传统 MVC 架构在,Service+数据模型的开发方式,会让 Service 扁平的、大量的,平铺出非常复杂的业务逻辑代码。再加上行为对象与功能逻辑的分离,贫血模型的开发方式,会让行为对象不断交叉引用,也是让系统不断增加复杂度,最终导致系统难以维护。所以这一阶段要设计每一个可以表达领域概念的模型,并运用实体、聚合、领域服务来承载
DDD的概念
充血模型
充血模型,指将对象的属性信息与行为逻辑聚合到一个类中,常用的手段如在对象内提供属于当前对象的信息校验、拼装缓存 Key、不含服务接口调用的逻辑处理等

这样的方式可以在使用一个对象时,就顺便拿到这个对象提供的一系列方法信息,所有使用对象的逻辑方法,都不需要自己再次处理同样的逻辑
但不要只是把充血模型,仅限于一个类的设计和一个类内的方法设计。充血还可以是整个包结构,一个包下包括了用于实现此包 Service 服务所需的各类零部件(模型、仓鼠、工厂),也可以被看作充血模型
同时还会在一个同类的类,提供对应的内部类,如用户实名,包括了,通信类、实名类、银行卡、四要素等。这些都被写入一个用户类下的内部子类,这样在编写代码时也会清晰看到子类的所属信息,更容易理解代码和维护
领域模型
领域模型,指特定业务领域内,业务规则、策略以及业务流程的抽象和封装。在设计手段上,通过风暴模型拆分领域模块,行程界限上下文。最大的区别在于把原有的众多 Service+数据模型的方法,拆分为独立的有边界的领域模块。每个领域内创建自身所属的;领域对象(实体、聚合、指对象)、仓储服务(Dao 操作)、工厂、端口适配器 Port(调用外部接口的手段)等

在原本的 Service + 贫血的数据模型开发指导下,Service 串联调用每一个功能模块。这些基础设施(对象、方法、接口)是被相互掉调用的。这也是因为贫血模型并没有面向对象的设计,所有的需求开发只有详细设计。
换到充血模型下,现在我们以一个领域功能为聚合,拆分一个领域内所需的 Service 为领域服务,VO、Req、Res 重新设计为领域对象,DAO、Redis 等持久化操作为仓储等。举例;一套账户服务中的,授信认证、开户、提额降额等,每一个都是一个独立的领域,在每个独立的领域内,创建自身领域所需的各项信息。
领域模型还有一个特点,它自身只关注业务功能实现,不与外部任何接口和服务直连。如;不会直接调用 DAO 操作库,也不会调用缓存操作 Redis,更不会直接引入 RPC 连接其他微服务。而是通过仓库和端口适配器,定义调用外部数据的含有出入参对象的接口标准,让基础设施层做具体的调用实现。通过这样的方式让领域只关心业务实现,同时做好防腐
实体、聚合、值对象
原本贫血模型下的开发,是不会特别在意一个方法的出入参的,经常很多个服务共用一个 VO 对象作为入参,只要这个对象能把需要的属性带进来就可以了
但是在 DDD 的领域模型设计下,领域对象的设计是非常面向对象的。而且在整个风暴时间的四色建模过程也是在以领域对象为驱动进行的,实体、聚合、值对象,三者位于每个领域下的领域对象内,服务与领域内的领域服务。三个对象的定义具体如下:
实体
是依托于持久化层数据以领域服务功能目标为指导设计的领域对象。持久化PO对象是原子类对象,不具有业务语义,而实体对象是具有业务语义且有唯一标识的对象,跟随于领域服务方法的全生命周期对象。如;用户PO持久化对象,会涵盖,用户的开户实体、授信实体、额度实体对象。也包括如商品下单时候的购物车实体对象。这个对象也通常是领域服务方法的入参对象。
概念:实体 = 唯一标识 + 状态属性 + 行为动作(功能),是DDD中的一个基本构建块,它代表了具有唯一标识的领域对象。实体不仅仅包含数据(状态属性),还包含了相关的行为(功能),并且它的标识在整个生命周期中保持不变。
- 概念:实体 = 唯一标识 + 状态属性 + 行为动作(功能),是DDD中的一个基本构建块,它代表了具有唯一标识的领域对象。实体不仅仅包含数据(状态属性),还包含了相关的行为(功能),并且它的标识在整个生命周期中保持不变。
- 特征:
- 唯一标识:实体具有一个可以区分其他实体的标识符。这个标识符可以是一个ID、一个复合键或者是一个自然键,关键是它能够唯一地标识实体实例。
- 领域标识:实体的标识通常来源于业务领域,例如用户ID、订单ID等。这些标识符在业务上有特定的含义,并且在系统中是唯一的。
- 委派标识:在某些情况下,实体的标识可能是由ORM(对象关系映射)框架自动生成的,如数据库中的自增主键。这种标识符虽然可以唯一标识实体,但它并不直接来源于业务领域。
- 用途:
- 表达业务概念:实体用于在软件中表达具体的业务概念,如用户、订单、交易等。通过实体的属性和行为,可以描述这些业务对象的特征和能力。
- 封装业务逻辑:实体不仅仅承载数据,还封装了业务规则和逻辑。这些逻辑包括验证数据的有效性、执行业务规则、计算属性值等。这样做的目的是保证业务逻辑的集中和一致性。
- 保持数据一致性:实体负责维护自身的状态和数据一致性。它确保自己的属性和关联关系在任何时候都是正确和完整的,从而避免数据的不一致性。
- 实现手段:
- 定义实体类:在代码中定义一个类,该类包含实体的属性、构造函数、方法等。
- 实现唯一标识:为实体类提供一个唯一标识的属性,如ID,并确保在实体的生命周期中这个标识保持不变。
- 封装行为:在实体类中实现业务逻辑的方法,这些方法可以操作实体的状态,并执行相关的业务规则。
- 使用ORM框架:利用ORM框架将实体映射到数据库表中,这样可以简化数据持久化的操作。
- 实现领域服务:对于跨实体或跨聚合的操作,可以实现领域服务来处理这些操作,而不是在实体中直接实现。
- 使用领域事件:当实体的状态发生变化时,可以发布领域事件,这样可以通知其他部分的系统进行相应的处理。
值对象
这个对象在领域服务方法的生命周期过程内是不可变对象,也没有唯一标识。它通常是配合实体对象使用。如为实体对象提供对象属性值的描述,比如;一个公司雇员的级别值对象,一个下单的商品收货的四级地址信息对象。所以在开发值对象的时候,通常不会提供 setter 方法,而是提供构造函数或者 Builder 方法来实例化对象。这个对象通常不会独立作为方法的入参对象,但做可以独立作为出参对象使用。
- 概念:值对象是由一组属性组成的,它们共同描述了一个领域概念。与实体(Entity)不同,值对象不需要有一个唯一的标识符来区分它们。值对象通常是不可变的,这意味着一旦创建,它们的状态就不应该改变。
- 特征:
- 不可变性(Immutability):值对象一旦被创建,它的状态就不应该发生变化。这有助于保证领域模型的一致性和线程安全性。
- 等价性(Equality):值对象的等价性不是基于身份或引用,而是基于对象的属性值。如果两个值对象的所有属性值都相等,那么这两个对象就被认为是等价的。
- 替换性(Replaceability):由于值对象是不可变的,任何需要改变值对象的操作都会导致创建一个新的值对象实例,而不是修改现有的实例。
- 侧重于描述事物的状态:值对象通常用来描述事物的状态,而不是事物的唯一身份。
- 可复用性(Reusability):值对象可以在不同的领域实体或其他值对象中重复使用。
- 用途:
- 金额和货币(如价格、工资、费用等)
- 度量和数据(如重量、长度、体积等)
- 范围或区间(如日期范围、温度区间等)
- 复杂的数学模型(如坐标、向量等)
- 任何其他需要封装的属性集合
- 实现手段:
- 定义不可变类:确保类的所有属性都是私有的,并且只能通过构造函数来设置。
- 重写equals和hashCode方法:这样可以确保值对象的等价性是基于它们的属性值,而不是对象的引用。
- 提供只读访问器:只提供获取属性值的方法,不提供修改属性值的方法。
- 使用工厂方法或构造函数创建实例:这有助于确保值对象的有效性和一致性。
- 考虑序列化支持:如果值对象需要在网络上传输或存储到数据库中,需要提供序列化和反序列化的支持。
聚合
当对数据库的操作需要使用到多个实体时,可以创建聚合对象。一个聚合对象,代表着一个数据库事务,具有事务一致性。聚合中的实体可以由聚合提供创建操作,实体也被称为聚合根对象。一个订单的聚合,会涵盖;下单用户实体对象、订单实体、订单明细实体和订单收货四级地址值对象。而那个作为入参的购物车实体对象,已经被转换为实体对象了。—— 聚合内事务一致性,聚合外最终一致性。
- 概念:聚合是领域模型中的一个关键概念,它是一组具有内聚性的相关对象的集合,这些对象一起工作以执行某些业务规则或操作。聚合定义了一组对象的边界,这些对象可以被视为一个单一的单元进行处理。
- 特征:
- 一致性边界:聚合确保其内部对象的状态变化是一致的。当对聚合内的对象进行操作时,这些操作必须保持聚合内所有对象的一致性。
- 根实体:每个聚合都有一个根实体(Aggregate Root),它是聚合的入口点。根实体拥有一个全局唯一的标识符,其他对象通过根实体与聚合交互。
- 事务边界:聚合也定义了事务的边界。在聚合内部,所有的变更操作应该是原子的,即它们要么全部成功,要么全部失败,以此来保证数据的一致性。
- 用途:
- 封装业务逻辑:聚合通过将相关的对象和操作封装在一起,提供了一个清晰的业务逻辑模型,有助于业务规则的实施和维护。
- 保证一致性:聚合确保内部状态的一致性,通过定义清晰的边界和规则,聚合可以在内部强制执行业务规则,从而保证数据的一致性。
- 简化复杂性:聚合通过组织相关的对象,简化了领域模型的复杂性。这有助于开发者更好地理解和扩展系统。
- 实现手段:
- 定义聚合根:选择合适的聚合根是实现聚合的第一步。聚合根应该是能够代表整个聚合的实体,并且拥有唯一标识。
- 限制访问路径:只能通过聚合根来修改聚合内的对象,不允许直接修改聚合内部对象的状态,以此来维护边界和一致性。
- 设计事务策略:在聚合内部实现事务一致性,确保操作要么全部完成,要么全部回滚。对于聚合之间的交互,可以采用领域事件或其他机制来实现最终一致性。
- 封装业务规则:在聚合内部实现业务规则和逻辑,确保所有的业务操作都遵循这些规则。
- 持久化:聚合根通常与数据持久化层交互,以保存聚合的状态。这通常涉及到对象-关系映射(ORM)或其他数据映射技术。
仓储和适配器
在 DDD 的设计方法中,领域吃做到了只关心领域服务实现,最能体现这样设计的就是仓库和适配器的设计。通常在 Service+数据模型的设计中,会在 Service 中引入 Redis、RPC、配置中心等各类其他外部服务。但在 DDD 中,通过仓储和适配器以及基础设施层的定义,解耦了这部分内容

- 特征:
- 封装持久化操作:Repository负责封装所有与数据源交互的操作,如创建、读取、更新和删除(CRUD)操作。这样,领域层的代码就可以避免直接处理数据库或其他存储机制的复杂性。
- 领域对象的集合管理:Repository通常被视为领域对象的集合,提供了查询和过滤这些对象的方法,使得领域对象的获取和管理更加方便。
- 抽象接口:Repository定义了一个与持久化机制无关的接口,这使得领域层的代码可以在不同的持久化机制之间切换,而不需要修改业务逻辑。
- 用途:
- 数据访问抽象:Repository为领域层提供了一个清晰的数据访问接口,使得领域对象可以专注于业务逻辑的实现,而不是数据访问的细节。
- 领域对象的查询和管理:Repository使得对领域对象的查询和管理变得更加方便和灵活,支持复杂的查询逻辑。
- 领域逻辑与数据存储分离:通过Repository模式,领域逻辑与数据存储逻辑分离,提高了领域模型的纯粹性和可测试性。
- 优化数据访问:Repository实现可以包含数据访问的优化策略,如缓存、批处理操作等,以提高应用程序的性能。
- 实现手段:
- 定义Repository接口:在领域层定义一个或多个Repository接口,这些接口声明了所需的数据访问方法。
- 实现Repository接口:在基础设施层或数据访问层实现这些接口,具体实现可能是使用ORM(对象关系映射)框架,如MyBatis、Hibernate等,或者直接使用数据库访问API,如JDBC等。
- 依赖注入:在应用程序中使用依赖注入(DI)来将具体的Repository实现注入到需要它们的领域服务或应用服务中。这样做可以进一步解耦领域层和数据访问层,同时也便于单元测试。
- 使用规范模式(Specification Pattern):有时候,为了构建复杂的查询,可以结合使用规范模式,这是一种允许将业务规则封装为单独的业务逻辑单元的模式,这些单元可以被Repository用来构建查询。
仓储解耦的手段使用了依赖倒置的设计,所有领域需要的外部服务,不在直接引入外部的服务,而是通过定义接口的方式,让基础设施层实现领域层接口(仓储/适配器)的方式来处理。
那么也就是基础设置层负责原则对接数据库、缓存、配置中心、RPC接口、HTTP接口、MQ推送等各项资源,并承接领域服务的接口调用各项服务为领域层提供数据能力。
同时这也会体现出,领域层的实现是具有业务语义的,而到了基础设置层则没有业务语义,都是原子的方法。通过原子方法的组合为领域业务语义提供支撑。
领域编排
在 DDD 中,每一个领域都是界限上下文拆分的独立结果,而实现业务流程的功能则需要串联各个领域模块提供一整条链路的完整服务。所以也常说领域内事务一致性,领域外最终一致性。
同时这些领域模块因为是独立的,所以也可以被复用。在不同的场景功能诉求下,可以选择不同的领域模块进行组装,这个过程就像搭积木一样。
但这里有一个取舍,如果项目相对来说并不大,也没有太多的编排处理。那么可以直接让触发器层对接领域层,减少编排层后,编码会更加便捷。
触发器
在所有的模型都定义完成后,领域业务被串联了。那么接下来则是使用,而使用的方式可以包括;接口(http/rpc)、消息监听、定时任务等方式,这些方式统一被定义为触发动作。
由触发发起对编排功能的调用处理。如;定时任务做信贷的计息、开户成功消息通知返利优惠券、提供接口让外部调用授信逻辑等。这些都是触发动作。
DDD建模方法
四色建模(风暴事件)是整个 DDD 中用于工程拆分界限上下文的非常重要的实践手段,通过建模过程快速识别业务领域中的关键时间和核心流程,也是在这个过程中设计出领域对象的,为后面详细设计和代码开发做指导
这个过程可以理解为,为工程开发提供面向对象设计,涵盖:领域拆分、界限串联、功能聚合。所以相比 Service+数据模型的贫血开发方式,DDD 前期要付出更多的设计成本,但对于软件的长周期迭代,这样的好处很值得
建模目的
工程建模的目的是为了做工程开发时提供指导方案,在工程开发时所需的各类核心内容都会在建模中体现,比如,分几个包、有哪些核心对象、要串联什么流程、有哪些核心业务要实现、过程中与外部服务的交互等
怎么建模
DDD 的建模,是以一个用户为起点,通过行为命令,发起行为动作,串联整个业务。而这个用户的起点最初来自用例图的分析。用例图是用户与系统交互的最简表示形式,展示了用户和与他相关的用例之间的联系。通过用例图,可以分析出所有的动作行为。在 DDD 中用于完成用户的行为命令和动作分析的过程,是一个四色建模的过程,也称作风暴模型。

上图是整个四色建模的指导图,通过寻找领域事件,发起事件命令,完成领域事件的过程,完成 DDD 工程建模
- 蓝色-决策命令,是用户发起的行为动作,比如:签到、抽奖、查看额度等
- 黄色-领域事件,过去时态描述,比如:签到完成、抽奖完成。他所阐述的都是这个领域要完成的终态
- 粉色-外部系统,比如你系统需要调用外部的接口完成流程
- 红色-业务流程,用于串联决策命令到领域事件,所实现的业务流程。一些简单的场景则直接由决策命令到领域事件就可以了
- 绿色-只读模型,做一些读取数据的动作,没有写库的操作
- 棕色-领域对象,每个决策命令的发起,都是含有一个对应的领域对象
上图左下角是一个示例图。一个用户,通过一个决策命令,使用领域对象,通过业务流程,完成两个领域事件,调用一次外部接口的过程。DDD 的建模过程就是在寻找这些节点
DDD工程模型
工程结构的存在作用,就是为了承载工程系统开发的模型划分,定义工程服务开发过程中实施标准
在通常意义的 MVC 下,开发过程所有需要的内容,都会堆砌在 Service 的实现类中,这样说为了 DDD 领域驱动设计的落地工程结构,会出现:洋葱架构、整洁架构、菱形架构、六边形架构等这些架构模型。因为需要更细致的划分,来承载 DDD 设计概念中映射的领域、仓储、视频、编排、触发,并重视面向对象过程
为什么需要架构
架构的核心目的就是让不同的职责分配到不同的区域内
在工程开发时会设计到的核心科目:

比如:统一的异常、数据的连接、日志的打印、外部服务的调用、消息的监听、任务的轮询以及服务的实现等一系列的东西要处理,分配到不同的工程包在承载,在 MVC 结构中,这些都是 Service 层在承接

在以往单体应用时代开发下,其实是没有这么多东西的,那时候的工程结构都偏向于 Service+贫血模型实现
但随着微服务的严谨,越来越多的内容填充到工程中,这时候会发现原本的 MVC 结构其实已经变得非常混乱了,一个 Service 为了实现自己的功能,要引入一堆东西,这些原子的 g0ongn 与 Service 自身的服务耦合在一起,也导致了工程的维护成本越来越大
工程结构设计
在《Domain Drive Design》这本著作中,有一个六边形关系图理论,与 DDD 的分层设计方式非常契合

无论是六边形架构还是洋葱架构,他们的目标都是以领域服务为核心,隔离内部实现与外部资源的耦合
在 DDD 分层架构下,以支撑 domain 核心领域实现拆分出基础设施(infrastructure),来成绩对外部资源的调用,触发器(trigger)向外部提供服务,之后 app 为应用启动,api 为接口定义,tyoes 为通用信息,case 为编排
这样一套结构下,用于开发工程的各项科目也可以被优雅的分配到各个分层结构了。这样的思想映射到工程随着,常见的分层结构有两天,一套是整洁分层,另一套是六边形分层
整洁架构

整洁架构的分包形式,会将所有的外部依赖使用和工程内要对外的,统一定义到适配器层,这里可以理解为对内适配和对外适配
六边形架构

六边形架构,会把自身提供到外部的放到 trigger,让接口调用、消息监听、任务调度,都可以统一一个入口处理,而对于需要调用外部同类的能力统一放到 infrastructure 基础设施层,包括:数据库、缓存、配置、调用其他方的接口
领域模型设计
虽然都是 DDD,也都有对应的模块和分包,但在细节之处还是会有一些差异,这个没有绝对的好与坏,按照自己的需求来。
分包方式
下面是两种分包方式

- 方式 1:DDD 领域科目类型分包,类型之下写每个业务逻辑
- 方式 2:业务领域分包,每个业务领域之下有自己所需的 DDD 领域科目
领域模型
DDD 领域驱动设计的核心,主要在于领域模型的设计,以领域所需驱动功能实现和数据建模。一个领域服务下面会有多个领域模型,每个领域模型都是一个充血结构。一个领域模型=一个充血结构

- model 模型对象:
- aggreate:聚合对象,实体对象、值对象的协调组长,就是聚合对象
- entity:实体对象,大多数情况在,实体对象(Entity)与数据库持久化对象(PO)是一对一关系,但也有为了封装一些属性信息,会出现一对多的关系
- valobj:值对象,通过对象属性值来识别的对象
- repository 仓储服务:从数据库等数据源中获取数据,传递的对象可以是聚合对象、实体对象,返回的结果可以说 实体对象、值对象。因为仓储服务是由基础层(infrastructure)引用领域层(domain),是一个依赖倒置的结构,但它可以天然的隔离 PO 数据库持久化对象被引用
- service 服务设计:这里要注意,不要以为定义了聚合对象,就把超宇一个对象以外的逻辑,都封装到聚合中,这会让代码后期越来越难以维护。聚合更应该注重的是和本对象相关的单一简单封装,一些重核心业务放到 service 里实现。此外,如何你的设计模式应用不加,那么无论是领域驱动设计、测试驱动设计还是别的架构,工程质量依然会非常差
分层调用链路
下面把 DDD 的分层架构平铺展开,看看层应该接口的实现到各个模块分层中的调用链路关系是什么样的,这样在做自己的代码开发时,也可以参考到底应该把什么样的功能分配到哪个模块中处理

从 App 层、触发器层、应用层,这三款主要是对领域层的上下文逻辑封装、触发式(MQ、HTTP、JOB)使用,并最终在应用层中打包发布上线。这一部分的都是使用的处理,所以不会有太复杂的操作