DDD(Domain Driver Design)领域驱动模型

Domain Primitive(DP)

DP概念
DP 是 DDD 中的一个基础概念,是 DDD 中可以执行的一个最小单元,最直接的体现是,将业务相关的参数定义在一个特定的领域中(比如一个 class 文件),封装成一个具有精准定义,自我验证,拥有行为的 ValueObject。

行为指相关业务代码

Value Object
区别于 Entity,拥有 id,是一个表的实例,而 VO 没有 id,更多的强调数据,不需要对应任何表,只是一个数据的集合,一个值对象,它的一个最大特点是 Immutable(不可变性),这个值对象自从被创建出来后不会被改变,所以说这个对象中的属性最好都是被 private final 修饰。

DP原则

  • 将隐性的概念显性化
    例:电话号是用户的一个属性,属于隐形概念,但实际上获取电话号的地区号(行为)才是真正的业务逻辑,因此需要将电话号的概念显性化,在“电话号”对象里实现该行为。
  • 将隐性的上下文显性化
    例:当我们做支付功能时,实际上需要的一个入参对象是支付金额 + 货币类型,因此可以把这两个概念组合成一个独立的完整概念:Money
  • 封装多对象行为
    例:如果一段业务逻辑涉及到多个对象,就需要用 DP 包装起来

什么情况可以考虑使用 DP 进行业务优化?
1 接口清晰度,通过接口参数是否能够显示业务
2 数据校验和错误处理是否统一维护,数据校验的依赖性,比如地区号的获取依赖于电话号,那地区号校验就要放在“电话号”类中做校验
3 业务逻辑代码的清晰度,胶水代码是否存在于核心业务代码中,区分核心业务代码和非核心业务代码(胶水代码:从一些入参里抽取一部分数据,然后调用一个外部依赖获取更多的数据,然后通常从新的数据中再抽取部分数据用作其他的作用。)


DDD应用架构

View Object(VO)-- 视图对象,代表展示层需要显示的数据,通常由 DTO 转换后展示。
Data Transfer Object(DTO)-- 数据传输对象,主要作为 Application 层的入参和出参。
Entity - 基于领域逻辑的实体类,拥有 ID,它的字段和数据库储存不需要有必然的联系,不仅包含数据,还有行为,字段也不仅仅是 String 等基础类型,而应该尽可能用 Domain Primitive 代替,可以避免大量的校验代码。
Data Object(DO)- DO 是单纯的和数据库表的映射关系,每个字段对应数据库表的一个 column,DO 只有数据,没有行为。
Repository - 只负责定义 Entity 对象的存储和读取方法,返回对象一般为 Entity。
RepositoryImpl - 实现数据库存储读取的细节,通过加入 Repository 接口,底层的数据库连接可以根据实际情况用不同的实现类来替换。
Mapper(DAO) - DAO 对应的是一个特定的数据库类型的操作,CRUD。
Builder/Factory - 实现 DO 与 Entity 之间的转化。
Anti-Corruption Layer(ACL)- 防腐层,很多时候我们的系统会去依赖其他的系统,而被依赖的系统可能包含不合理的数据结构、API、协议或技术实现,如果对外部系统强依赖,会导致我们的系统被”腐蚀“。这个时候,通过在系统间加入一个防腐层,能够有效的隔离外部依赖和内部逻辑,无论外部如何变更,内部代码可以尽可能的保持不变。

ACL作用

  • 适配器:很多时候外部依赖的数据、接口和协议并不符合内部规范,通过适配器模式,可以将数据转化逻辑封装到 ACL 内部,降低对业务代码的侵入。比如转化对方的入参和出参,序列化反序列化等,让入参出参更符合我们的标准。
  • 缓存:对于频繁调用且数据变更不频繁的外部依赖,通过在 ACL 里嵌入缓存逻辑,能够有效的降低对于外部依赖的请求压力。同时,很多时候缓存逻辑是写在业务代码里的,通过将缓存逻辑嵌入 ACL ,能够降低业务代码的复杂度。
  • 兜底:如果外部依赖的稳定性较差,一个能够有效提升我们系统稳定性的策略是通过 ACL 起到兜底的作用,比如当外部依赖出问题后,返回最近一次成功的缓存或业务兜底数据。这种兜底逻辑一般都比较复杂,如果散落在核心业务代码中会很难维护,通过集中在 ACL 中,更加容易被测试和修改。
  • 功能开关:有些时候我们希望能在某些场景下开放或关闭某个接口的功能,或者让某个接口返回一个特定的值,我们可以在 ACL 配置功能开关来实现,而不会对真实业务代码造成影响。

ACL 原理

  • 对于依赖的外部对象,我们抽取出所需要的字段,生成一个内部所需的 VO 或 DTO 类
  • 构建一个新的 Facade(GateWay),在 Facade 中封装调用链路,将外部类转化为内部类
  • 针对外部系统调用,同样的用 Facade 方法封装外部调用链路

Domain Layer(领域层):Entity、Domain Primitive 和 Domain Service 都属于领域层,领域层没有任何外部依赖关系。Domain Primitive 是无状态的。当某个行为影响到多个 Entity 时,属于跨实体的业务逻辑,在这种情况下就需要通过一个第三方的领域服务(Domain Service)来完成。
Application Layer(应用层):Application Service、Repository、ACL 类属于应用层,应用层依赖领域层,但不依赖具体实现。
Infrastructure Layer(基础设施层):ACL,Repository 等的具体实现类,通常依赖外部具体的技术实现和框架。
Domain-Driven Design(DDD):领域驱动设计,架构思路是先写 Domain 层的业务逻辑,然后再写 Application 层的组件编排,最后才写每个外部依赖的具体实现。

解耦实现


DTO Assembler:将 1 个或多个相关联的 Entity 转化为 1 个或多个 DTO。
Data Converter:Entity 到 DO 的转化器。
转换一般使用 MapStruct 库

Repository:当成一个中性的类,使用语法如 find、save、remove,使用中性的 save 接口,然后在具体实现上根据情况调用 DAO 的 insert 或 update 接口,根据 Aggregate 的 ID 是否存在且大于 0 来判断一个 Aggregate 是需要更新还是插入。这样做是为了概念上和数据库解绑,不去直接使用 insert、select、update、delete。
Repository 复杂实现:一次操作中,并不是所有 Aggregate 里的 Entity 都需要变更,但是如果用简单的写法,会导致大量的无用 DB 操作。具体实现参照:Repository复杂实现

领域层设计规范

Entity
通过聚合根保证主子实体的一致性
在稍微复杂一点的领域里,通常主实体会包含子实体,这时候主实体就需要起到聚合根的作用,即:

  • 子实体不能单独存在,只能通过聚合根的方法获取到。任何外部的对象都不能直接保留子实体的引用。
  • 子实体没有独立的 Repository,不可以单独保存和取出,必须要通过聚合根的 Repository 实例化。
  • 子实体可以单独修改自身状态,但是多个子实体之间的状态一致性需要聚合根来保障。

不可以强依赖其他聚合根实体或领域服务
一个实体类不能直接在内部直接依赖一个外部的实体或服务。正确的对外部依赖的方法有两种:

  • 只保存外部实体的 ID:强烈建议使用强类型的 ID 对象,而不是 Long 型 ID。强类型的 ID 对象不单单能自我包含验证代码,保证 ID 值的正确性,同时还能确保各种入参不会因为参数顺序变化而出 bug。
  • 针对于“无副作用”的外部依赖,通过方法入参的方式传入。如果方法对外部依赖有副作用,不能通过方法入参的方式,只能通过 Domain Service 解决。

任何实体的行为只能直接影响到本实体(和其子实体),不能直接修改其他的实体类。

分层理解

Interface层
1 网络协议的转化:通常这个已经由各种框架给封装掉了,我们需要构建的类要么是被注解的 bean,要么是继承了某个接口的 bean。
2 统一鉴权:比如在一些需要 AppKey+Secret 的场景,需要针对某个租户做鉴权的,包括一些加密串的校验
3 Session 管理:一般在面向用户的接口或者有登陆态的,通过 Session 或者 RPC 上下文可以拿到当前调用的用户,以便传递给下游服务。
4 限流配置:对接口做限流避免大流量打到下游服务
5 前置缓存:针对变更不是很频繁的只读场景,可以前置结果缓存到接口层
6 异常处理:通常在接口层要避免将异常直接暴露给调用端,所以需要在接口层做统一的异常捕获,转化为调用端可以理解的数据格式
7 日志:在接口层打调用日志,用来做统计和 debug 等。一般微服务框架可能都直接包含了这些功能。

规范:

  • Interface 层的 HTTP 和 RPC 接口,返回值为 Result,捕捉所有异常。
  • 一个 Interface 层的类应该是“小而美”的,应该是面向“一个单一的业务”或“一类同样需求的业务”,需要尽量避免用同一个类承接不同类型业务的需求。

Application层
1 ApplicationService 应用服务:最核心的类,负责业务流程的编排,但本身不负责任何业务逻辑。
2 DTO Assembler:负责将内部领域模型转化为可对外的 DTO。
3 Command、Query、Event 对象:作为 ApplicationService 的入参。
4 返回的DTO:作为 ApplicationService 的出参。

Command、Query、Event对象

CQE vs DTO
CQE:CQE 对象是 ApplicationService 的输入,是有明确的“意图”的,所以这个对象必须保证其“正确性”。
DTO:DTO 对象只是数据容器,只是为了和外部交互,所以本身不包含任何逻辑,只是贫血对象。

规范

  • Application 层的所有接口返回值为 DTO,不负责处理异常,可以直接抛异常,不用统一处理。
  • ApplicationService 的接口入参只能是一个 Command、Query 或 Event 对象,CQE 对象需要能代表当前方法的语意。唯一可以的例外是根据单一ID查询的情况,可以省略掉一个 Query 对象的创建。
  • CQE 对象的校验应该前置,避免在 ApplicationService 里做参数的校验。可以通过 JSR303/380 和 Spring Validation 来实现。
  • 针对于不同语意的指令,要避免 CQE 对象的复用,比如常见的场景是“Create 创建”和“Update 更新”应该分开。

Application Service 是业务流程的封装,不处理业务逻辑
判断是否业务流程的几个点:
1 不要有 if/else 分支逻辑:通常有分支逻辑的,都代表一些业务判断,应该将逻辑封装到 Domain Service 或者 Entity 里,但这不代表完全不能有 if 逻辑,比如:

ItemDO item = itemService.getItem(cmd.getItemId());
if (item == null) {
    throw new IllegalArgumentException("Item not found");
}

但这里仅仅代表了中断条件,具体的业务逻辑处理并没有受影响。
2 不要有任何计算逻辑。
3 数据的转化交给其他对象来做,数据的转化可以交给其他对象来做。

COLA4.0架构图


1 适配层(Adapter Layer):负责对前端展示的路由和适配,对于传统B/S系统而言,adapter 就相当于 MVC 中的 controller,包括 vo、和assembler 类似的转换类(实现 vo 与 dto 之间转换);
2 应用层(Application Layer):主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的,应用层也可以绕过领域层,直接访问基础实施层(利用 mapper 访问),包括 executorImpl、assembler(实现 dto 与 model 之间转换或 dto 与 po 之间转换)、dto、rpc(实现 Client 中的 facade);
3 领域层(Domain Layer):主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Domain Entity)的方法对 App 层提供业务实体和业务逻辑计算。领域是应用的核心,不依赖任何其他层次,包括abilityImpl;
4 基础实施层(Infrastructure Layer):主要负责技术细节问题的处理,比如数据库的 CRUD、搜索引擎、文件系统、分布式服务的 RPC 等。此外,领域防腐的重任也落在这里,外部依赖需要通过 gateway 的转义处理,才能被上面的Domain层使用,包括po、converter(实现 model 与 po 之间转换);
5 Client(封装成 SDK 供外部调用):api(facade)、dto;

各个包结构的简要功能描述

posted @ 2021-12-12 15:17  这个杀手冷死了  阅读(1843)  评论(0编辑  收藏  举报