浅显易懂的 DDD 理解

178 阅读13分钟

下文是我对于领域驱动设计(Domain-Driven Design,简称 DDD)的理解,虽然浅显但是比较易懂,欢迎关注点赞。原创。

阿吉引言

DDD 是一种软件开发的方法,通过“统一语言”来使得业务领域的概念和逻辑在软件设计中更加清晰,使系统能够更好的和业务需求对齐,同时降低日后代码的迭代成本。

“统一语言”的意思就是专家和开发人员之间明确、通用、基于业务领域的语言,彼此之间知道对方在讲什么,并且彼此之间使用“统一语言”进行交流。

因此 DDD 的目标就是清晰反映 业务领域 和 提高软件的可维护性和可拓展性。

DDD 涉及到了两个设计,战略设计和战术设计,先进行战略设计,包括有界上下文划分、上下文映射、子域类型确定,然后进行战术设计,确定领域下的实体、值对象、聚合,领域服务,仓储。

MVC 和 DDD

MVC 是三层架构,controller 调用 service,service 调用 dao,然后结果返回给 controller。

使用 MVC 的过程中,导致代码的腐化问题严重,数据对象由于业务需要或者业务升级,对象属性不断变多,对象的“状态”(属性)和“行为”(操作)分离越来越严重,腐化问题越来越严重,对象膨胀的严重,对对象的操作方法也变的臃肿和分散,代码的意图随着时间越来越不清晰。“状态”和“操作”的分离,就是使用了贫血模型。

而 DDD 架构首先解决的就是上面的问题“状态和行为分离”,将属于自己领域的“状态”和“操作”封装到自己的领域包下进行处理,也就是说,对象和关于对象的操作是放在一起的,是 DDD 架构的设计精髓之一。“状态”和
“操作”放在一起,就是充血模型。同时所谓的战略和战术最明显的区别就是,战略定义“域”的划分,战术定义“域”内 model、service 和 repository。

领域

上面的内容提到了领域的概念,那就来说说什么是领域

首先领域的划分我认为是专家划分出来的,因为领域是一个特定的业务范围或者说知识区域,需要一定的专业背景和经验才能够确定。比如教育公司中的课程研发和课程服务就是核心领域。

然后就是领域的特点,专业性、边界性、动态性,也比较好理解,专业性就是说这个领域是专家划分的,很专业,边界性就是这个领域有明确的边界,比如电商系统中的商品域和订单域有着明显的界限,动态性也就是说随着业务和市场的变化,这个领域内部的东西是变化的。

领域还涉及到了分类,核心领域、支撑领域、通用领域。

核心领域就是业务的关键部分,也就是重要的域,比如教育公司的课程研发领域,就是很重要的一个领域。

支撑领域就是给核心领域进行支持和辅助的,比如教育公司的用户领域。

通用领域,没有业务特性,具有广泛的通用性和可复用性,比如日志领域、邮件领域。

有界上下文

上面内容还提到了“界”这个概念,就是字面的意思“界限”,不要想复杂。

有界上下文划分,指的就是划定一个界限范围,这个界限范围内的领域模型、业务规则具有一致性,在这个范围内,大家对于业务的理解和定义必须一致。目的就是降低系统的复杂度,在大的业务领域划分成了有界限的小的业务领域。

划分的方法有三种,基于业务功能、组织架构、流程阶段。

基于业务功能:电商系统中,订单管理、用户管理、商品管理可以作为三个有界上下文

基于组织架构:如果开发团队的组织结构和业务功能有对应关系,按照团队分工来进行划分,比如开发小组 1 开发某个业务功能,这个业务功能就是一个有界上下文。

基于流程阶段:下单前,下单中,下单后,也可以作为三个有界上下文。

上下文映射

上面提到了有界上下文,划分了不同的界限范围,那他们如何进行交互呢?

因此有了“上下文映射”这个概念,用来描述有界上下文之间的关系和交互方式。目的就是为了解决上面提到的问题,确定他们彼此之间怎么交互,进而确保系统的完整性,因为系统是由多个有界上下文组装而成。

映射的方式有共享内核、客户-供应商、尊奉者、防腐层、开放主机服务。

共享内核:两个有界上下文共享一部分内容,比如用户管理和订单管理共享用户信息,用户信息就是共享内核,但是需要确保共享内核高度的一致性和同步性

客户-供应商:一个有界上下文为另一个有界上下文提供服务或者功能。比如商品管理为订单管理提供商品查询功能,商品管理就是供应商,订单管理就是客户。

尊奉者:一个有界上下文遵循另一个有界上下文的模型和接口。如果是一个企业系统,财务系统需要遵循人力系统对于员工信息的定义和格式,不能自己定义。

防腐层:两个有界上下文之间加一个转换层,防止彼此之间的模型概念和规则被污染,不直接交互。

开放主机服务:最好理解的一个方式,一个有界上下文直接定义一个服务接口,从而使得其他有界上下文可以进行调用,比如库存管理有界上下文开放自己的库存查询接口,订单管理有界上下文可以通过这个接口进行查询库存。

DDD 分层

在 MVC 中,controller 用来接收请求,service 用来处理具体的逻辑然后调用 dao 完成对于对象的操作。

在 DDD 架构中有 6 层

  • 接口定义 -api:因为微服务中引用的 RPC 需要对外提供接口的描述信息,也就是调用方在使用的时候,需要引入 Jar 包,让调用方好能依赖接口的定义做代理。
  • 应用封装 -app:这是应用启动和配置的一层,如一些 aop 切面或者 config 配置,以及打包镜像都是在这一层处理。你可以把它理解为专门为了启动服务而存在的。
  • 领域封装 -domain:领域模型服务,是一个非常重要的模块。无论怎么做DDD的分层架构,domain 都是肯定存在的。在一层中会有一个个细分的领域服务,在每个服务包中会有【模型、仓库、服务】这样3部分。这一层完成对数据操作的定义。
  • 仓储服务 -infrastructure:基础层依赖于 domain 领域层,因为在 domain 层定义了仓储接口需要在基础层实现。这是依赖倒置的一种设计方式。这一层完成对数据的操作。
  • 领域封装 -trigger:触发器层,一般也被叫做 adapter 适配器层。用于提供接口实现、消息接收、任务执行等。所以对于这样的操作,把它叫做触发器层。对外接口由该层来提供,实现了 api 层的接口定义。
  • 类型定义 -types:通用类型定义层,在我们的系统开发中,会有很多类型的定义,包括;基本的 Response、Constants 和枚举。它会被其他的层进行引用使用。
  • 领域编排【可选】 -case:领域编排层,一般对于较大且复杂的的项目,为了更好的防腐和提供通用的服务,一般会添加 case/application 层,用于对 domain 领域的逻辑进行封装组合处理。

对于这 6 层相对直白的理解就是:

app 层做 config 和应用启动,然后 trigger 层调用了在 api 层的接口,对外提供接口服务。

api 层的接口调用 domain 层的领域模型服务,领域模型服务中封装有各个领域包,领域包内含有 model、repository、service。

领域包内的 model 中有值对象和实体对象还有一个聚合对象。

实体是有唯一标识的对象,比如客户实体,具有唯一标识,而值对象没有唯一标识,只是为了满足某种需要而创建的对象,可能和实体对象中的部分属性有关也可能无关,看实际需要而定,用来描述一个领域概念。

而聚合对象则是包含了实体对象和值对象的一个对象,用来统一控制这两个对象。比如订单的聚合包括用户实体对象、订单实体对象、订单明细实体对象、收货地址值对象,对于聚合对象的操作是原子性的。model 中还会有自己的方法,这些方法不需要借助其他实体对象完成、也不会调用别的接口,比如一个用户实体,里面可能会有根据出生年月日判断年龄区间的方法,这个方法就可以放在实体中完成。

最后就是值对象,比如上文提到的地址,这个值没有唯一标识,xx 省 xx 市就是一个值对象。

service 包下放着的是领域服务,这个里面就是实体中无法完成的逻辑处理,可能涉及到多个实体、值对象以及接口调用等操作,就需要放到这个包下,领域服务中的数据处理调用 repository 包下的接口。

respository 包下的接口中定义了 service 中需要对数据进行处理的调用,这些数据操作定义在了仓储层中,依赖倒置,以前是数据操作使用数据对象,现在是数据对象使用数据操作方法,在 domain 层不进行任何的直接数据操作,只进行调用。

infrastructure 基础层,repopsitory 包下的类实现了对应的 domain 层中的 repository 中的接口,会定义 dao 和 po,dao 中定义了对于 po 的操作,mapper 文件放在了 app 层。

还有一个就是所谓的领域编排层,根据不同的场景功能诉求对领域进行组装。

整理一下就是:

app 启动---》trigger 层调用 api 层---》api 层调用 domain 层---》domain 层调用 infrastructure 层

个人对于 DDD 这么做的理解

  1. 首先明确了“域”,开发的时候界限更加明细、方法服务等也更加明细、不需要考虑过多同其他域的关联。“战略设计”属于大处着眼,“战术设计”属于小处着手。
  2. 其次就是将对象“状态”和“行为”写在一起的操作,也就是“充血模型”,能够使得对于这个对象有什么和能干什么也更加清晰。需要什么实体就单独定制一个实体出来,然后编写 service 和 repository 进行实体数据的操作,不会像 MVC 那样在一个类上面一直扩充属性。
  3. “统一语言”确保了开发和客户对业务概念、术语和流程都有一直的理解和表述,减少因为理解偏差导致的错误。大家彼此之间能够听懂对方说的是什么。
  4. “有界上下文”和“上下文划分”,使得边界明了,同时不同“有界上下文”之间的交互也做了明确定义,对于防腐和明确业务有着很大的帮助。

领域驱动设计(DDD)解决的问题主要有以下几个方面

一、复杂业务的清晰建模

  1. 解决业务理解不一致问题:在大型项目中,不同的角色(如开发人员、业务分析师、产品经理等)对业务的理解可能存在偏差。DDD 通过统一语言(Ubiquitous Language)的建立,确保所有团队成员基于相同的业务术语和概念进行交流和工作,减少误解和歧义。例如,在一个电商系统中,对于“订单”的定义和理解,通过统一语言,让开发人员、业务人员、测试人员等都能明确其内涵和外延。
  2. 提高业务模型的准确性:DDD 强调深入理解业务领域,挖掘业务的核心概念和规则,建立与实际业务紧密贴合的模型。这使得系统能够更准确地反映业务需求,减少因模型与业务实际不符导致的错误和返工。以金融交易系统为例,如果没有准确的业务模型,可能会导致交易流程、资金结算等方面出现错误。

二、系统的可维护性和扩展性

  1. 降低系统的耦合度:DDD 将系统划分为多个有边界的领域,每个领域专注于自身的业务逻辑,领域之间通过明确的接口进行交互。这种方式使得不同领域之间的耦合度降低,当需要对某个领域进行修改或扩展时,对其他领域的影响较小。比如,在一个物流管理系统中,订单管理领域和运输管理领域可以相对独立地进行开发和维护。
  2. 增强系统的扩展性:DDD 中的领域模型是基于业务的核心概念和规则构建的,具有较高的稳定性。当业务需求发生变化时,可以基于现有的领域模型进行扩展和调整,更容易满足新的业务需求。例如,一个社交网络系统,最初只支持文本内容的分享,当需要扩展支持图片、视频等内容分享时,可以在现有的内容分享领域模型基础上进行扩展。

三、提升开发效率和质量

  1. 提高开发效率:由于 DDD 建立了清晰的业务模型和统一语言,开发人员能够更快地理解业务需求,减少需求分析和设计阶段的时间成本。同时,低耦合的领域划分和清晰的架构,也使得开发过程更加流畅,提高开发效率。以一个企业资源计划(ERP)系统为例,通过 DDD 可以快速明确各个业务模块(如采购、销售、库存管理等)的边界和职责,开发人员可以并行开发,提高效率。
  2. 提升代码质量:DDD 鼓励采用面向对象的设计思想和原则,将业务逻辑封装在领域对象中,使得代码具有更高的内聚性和可读性。同时,通过领域模型的验证和约束,能够提前发现和解决潜在的业务逻辑错误,提高代码的质量和稳定性。比如,在一个在线教育系统中,课程管理领域的课程对象封装了课程的创建、更新、删除等逻辑,并且通过领域规则进行约束,保证了课程数据的一致性和准确性。