DDD 领域驱动学习

实体,聚合根,界限上下文

DP

class + type

  • Type 指我们在今后的代码里可以通过 PhoneNumber 去显性的标识电话号这个概念
  • Class 指我们可以把所有跟电话号相关的逻辑完整的收集到一个文件里
  1. 接口的清晰度
  2. 数据验证和错误处理
  3. 业务代码清晰度
  4. 可测试性

DP原则:

  1. 将隐性的概念显性化
  2. 让隐性的上下文显性化
  3. 封装 多对象 行为

应用架构

  1. 可维护性差
  2. 可扩展性差
  3. 可测试性差

违背原则

  1. 单一职责
  2. 依赖倒置
  3. 开笔原则

重构

  1. 抽象数据存储层
    • DO 针对数据库对象
    • Entity 针对逻辑领域内实体类,和数据库没直接关系,包含数据和行为,字段不仅仅是基础类型,还能是显示包装类
  2. 抽象第三方服务
    • 防腐层
      • 适配器
      • 缓存
      • 兜底
      • 易于测试
      • 功能开关
  3. 抽象中间件
    • 隔离底层实现
  4. 封装业务逻辑
    • entity 封装单对象有状态的行为,包括业务校验
    • 使用 Domain 封装多对象逻辑
  5. 代码组织结构

模式

为什么用Repository

  • 实体模型&贫血模型

Repository代码规范

  1. 接口名称不应该使用底层实现的语法(insert, update, delete 换成 find,save,remove这种语法,与SQL语法区分开)
  2. 出入参不应该用底层数据格式 (操作聚合根,Entity对象)
  3. 应该避免所谓的 "通用" Repository模式

贫血模型缺陷

  • 无法保护模型对象的完整性和一致性
  • 对象发现的可操作性极差
  • 代码校验逻辑重复
  • 代码的健壮差
  • 强依赖底层实现

为什么贫血模型这么多

  • 数据库思维,CRUD
  • 贫血模型简单
  • 脚本思维

根本原因 概念混淆

  • 数据模型:指业务数据该如何持久化,以及数据之间的关系,也就是传统的ER模型
  • 业务模型/领域模型:业务逻辑中,相关联的数据该如何联动

模型概念

  • DO 数据对象
  • Entity 实体对象
  • DTO 传输对象 -> CQE

转换

  • DTO Assembler -> 应用层概念 -> Entity 转换到 DTO, 1:1 或者 N:N
  • Data Converter -> Infrastructure层概念 -> Entity 转换到 DO

如何避免写流水账代码

方案

  1. 分离出独立的Interface接口层,负责处理网络协议相关的逻辑
  2. 从真实业务场景中,找出具体用例(Use Cases),然后将具体用例通过专用的Command指令、Query查询、和Event事件对象来承接
  3. 分离出独立的Application应用层,负责业务流程的编排,响应Command、Query和Event。每个应用层的方法应该代表整个业务流程中的一个节点
  4. 处理一些跨层的横切关注点,如鉴权、异常处理、校验、缓存、日志等

接口层组成

  1. 网络协议转化
  2. 统一鉴权
  3. Session 管理
  4. 限流配置
  5. 前置缓存
  6. 异常处理
  7. 日志

返回值和异常处理规范 Result & Exception

规范

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

Application层

ApplicationService应该永远返回DTO而不是Entity

核心类

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

Command、Query、Event对象

  • Command指令:指调用方明确想让系统操作的指令,其预期是对一个系统有影响,也就是写操作。通常来讲指令需要有一个明确的返回值(如同步的操作结果,或异步的指令已经被接受)
  • Query查询:指调用方明确想查询的东西,包括查询参数、过滤、分页等条件,其预期是对一个系统的数据完全不影响的,也就是只读操作。
  • Event事件:指一件已经发生过的既有事实,需要系统根据这个事实作出改变或者响应的,通常事件处理都会有一定的写操作。事件处理器不会有返回值。这里需要注意一下的是,Application层的Event概念和Domain层的DomainEvent是类似的概念,但不一定是同一回事,这里的Event更多是外部一种通知机制而已。

为什么要用CQE对象

传统接口的问题

  1. 接口膨胀,一个查询一个方法
  2. 难以扩展,每新增一个参数都有可能需要调用方升级
  3. 难以测试,接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护
  4. 最重要的问题,这些参数只是一些罗列值,没有任何业务上的 语义,无法明确表达意图

CQE规范

  • ApplicationService的接口入参只能是一个Command、Query或Event对象,CQE对象需要能代表当前方法的语意。唯一可以的例外是根据单一ID查询的情况,可以省略掉一个Query对象的创建

CQE校验

  • CQE对象的校验应该前置,避免在ApplicationService里做参数的校验。可以通过JSR303/380和Spring Validation来实现
  • 可以放在CQE对象内部做基础校验

CQE复用

  • 规范:针对于不同语意的指令,要避免CQE对象的复用

判断是否业务流程的几点

  1. 不要有if/else分支逻辑:也就是说代码的Cyclomatic Complexity(循环复杂度)应该尽量等于1
  2. 不要有任何计算,把计算逻辑封装在实体里
  3. 一些数据的转化可以交给其他对象来做

常见的Application套路

​ 我们可以看出来,ApplicationService的代码通常有类似的结构:AppService通常不做任何决策(Precondition除外),仅仅是把所有决策交给DomainService或Entity,把跟外部交互的交给Infrastructure接口,如Repository或防腐层。

套路如下

  1. 准备数据:包括从外部服务或持久化源取出相对应的Entity、VO以及外部服务返回的DTO。
  2. 执行操作:包括新对象的创建、赋值,以及调用领域对象的方法对其进行操作。需要注意的是这个时候通常都是纯内存操作,非持久化。
  3. 持久化:将操作结果持久化,或操作外部系统产生相应的影响,包括发消息等异步操作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值