实体,聚合根,界限上下文
DP
class + type
- Type 指我们在今后的代码里可以通过 PhoneNumber 去显性的标识电话号这个概念
- Class 指我们可以把所有跟电话号相关的逻辑完整的收集到一个文件里
- 接口的清晰度
- 数据验证和错误处理
- 业务代码清晰度
- 可测试性
DP原则:
- 将隐性的概念显性化
- 让隐性的上下文显性化
- 封装 多对象 行为
应用架构
- 可维护性差
- 可扩展性差
- 可测试性差
违背原则
- 单一职责
- 依赖倒置
- 开笔原则
重构
- 抽象数据存储层
- DO 针对数据库对象
- Entity 针对逻辑领域内实体类,和数据库没直接关系,包含数据和行为,字段不仅仅是基础类型,还能是显示包装类
- 抽象第三方服务
- 防腐层
- 适配器
- 缓存
- 兜底
- 易于测试
- 功能开关
- 防腐层
- 抽象中间件
- 隔离底层实现
- 封装业务逻辑
- entity 封装单对象有状态的行为,包括业务校验
- 使用 Domain 封装多对象逻辑
- 代码组织结构
模式
为什么用Repository
- 实体模型&贫血模型
Repository代码规范
- 接口名称不应该使用底层实现的语法(insert, update, delete 换成 find,save,remove这种语法,与SQL语法区分开)
- 出入参不应该用底层数据格式 (操作聚合根,Entity对象)
- 应该避免所谓的 "通用" Repository模式
贫血模型缺陷
- 无法保护模型对象的完整性和一致性
- 对象发现的可操作性极差
- 代码校验逻辑重复
- 代码的健壮差
- 强依赖底层实现
为什么贫血模型这么多
- 数据库思维,CRUD
- 贫血模型简单
- 脚本思维
根本原因 概念混淆
- 数据模型:指业务数据该如何持久化,以及数据之间的关系,也就是传统的ER模型
- 业务模型/领域模型:业务逻辑中,相关联的数据该如何联动
模型概念
- DO 数据对象
- Entity 实体对象
- DTO 传输对象 -> CQE
转换
- DTO Assembler -> 应用层概念 -> Entity 转换到 DTO, 1:1 或者 N:N
- Data Converter -> Infrastructure层概念 -> Entity 转换到 DO
如何避免写流水账代码
方案
- 分离出独立的Interface接口层,负责处理网络协议相关的逻辑
- 从真实业务场景中,找出具体用例(Use Cases),然后将具体用例通过专用的Command指令、Query查询、和Event事件对象来承接
- 分离出独立的Application应用层,负责业务流程的编排,响应Command、Query和Event。每个应用层的方法应该代表整个业务流程中的一个节点
- 处理一些跨层的横切关注点,如鉴权、异常处理、校验、缓存、日志等
接口层组成
- 网络协议转化
- 统一鉴权
- Session 管理
- 限流配置
- 前置缓存
- 异常处理
- 日志
返回值和异常处理规范 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对象
传统接口的问题
- 接口膨胀,一个查询一个方法
- 难以扩展,每新增一个参数都有可能需要调用方升级
- 难以测试,接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护
- 最重要的问题,这些参数只是一些罗列值,没有任何业务上的 语义,无法明确表达意图
CQE规范
- ApplicationService的接口入参只能是一个Command、Query或Event对象,CQE对象需要能代表当前方法的语意。唯一可以的例外是根据单一ID查询的情况,可以省略掉一个Query对象的创建
CQE校验
- CQE对象的校验应该前置,避免在ApplicationService里做参数的校验。可以通过JSR303/380和Spring Validation来实现
- 可以放在CQE对象内部做基础校验
CQE复用
- 规范:针对于不同语意的指令,要避免CQE对象的复用
判断是否业务流程的几点
- 不要有if/else分支逻辑:也就是说代码的Cyclomatic Complexity(循环复杂度)应该尽量等于1
- 不要有任何计算,把计算逻辑封装在实体里
- 一些数据的转化可以交给其他对象来做
常见的Application套路
我们可以看出来,ApplicationService的代码通常有类似的结构:AppService通常不做任何决策(Precondition除外),仅仅是把所有决策交给DomainService或Entity,把跟外部交互的交给Infrastructure接口,如Repository或防腐层。
套路如下
- 准备数据:包括从外部服务或持久化源取出相对应的Entity、VO以及外部服务返回的DTO。
- 执行操作:包括新对象的创建、赋值,以及调用领域对象的方法对其进行操作。需要注意的是这个时候通常都是纯内存操作,非持久化。
- 持久化:将操作结果持久化,或操作外部系统产生相应的影响,包括发消息等异步操作。