领域驱动设计落地
- 写模型代码结构
- interfaces 用户接口层
- facade 接口
- assembler 组装器
- application 应用层
- service 应用层服务
- domain 领域层
- Xxx 聚合
- entity 领域对象
- aggregate 聚合根
- entity 实体
- valobj 值对象
- repository 仓储
- po 持久化对象
- mapper
- service
- facade 仓储接口
- persistence 仓储实现
- infrastructure 基础层
- util 工具
- constant 常量
- 读模型代码结构
- 读写模型主要差异
- 问题汇总
写模型代码结构
com.myj.drug.xxx
├─Application.java
├─interfaces
| ├─facade
| | └XxxController.java
| ├─assembler
| | └PersonAssembler.java
├─infrastructure
| ├─util
| ├─constant
├─domain
| ├─Xxx
| | ├─service
| | | ├─XxxDomainService.java
| | | └XxxFactory.java
| | ├─repository
| | | ├─po
| | | | └XxxPO.java
| | | ├─persistence
| | | | └XxxRepositoryImpl.java
| | | ├─mapper
| | | | └XxxMapper.java
| | | ├─service
| | | | └XxxServiceImpl.java
| | | ├─facade
| | | | └XxxRepositoryInterface.java
| | ├─event
| | | ├─XxxEvent.java
| | | └XxxEventType.java
| | ├─entity
| | | ├─aggregate
| | | ├─entity
| | | ├─valobj
├─application
| ├─service
| | └XxxApplicationService.java
interfaces 用户接口层
用户接口层是前端应用与微服务应用层的桥梁,通过 Facade 接口封装应用服务,适配前端并提供灵活的服务,完成 DO 和
DTO(Query/Command) 相互转换
当应用服务接收到前端请求数据时,组装器会将 DTO(Query/Command) 转换为 DO。当应用服务向前端返回数据时,组装器会将
DO 转换为 DTO。
facade 接口
实际上对应的就是三层架构的Controller层
assembler 组装器
将 DTO(Query/Command) 转换为 DO 或者 DO 转换为 DTO(Query/Command) 的工具类
application 应用层
应用层位于领域层之上,主要负责组合和编排领域层服务
应用层也是微服务之间交互的通道,它可以调用其它微服务的应用服务,完成微服务之间的服务组合和编排
service 应用层服务
调用领域层服务或者调用其他微服务的服务
domain 领域层
领域层的作用是实现企业核心业务逻辑,通过各种校验手段保证业务的正确性。领域层主要体现领域模型的业务能力,它用来表达业务概念、业务状态和业务规则。
领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。
领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。
实体主要实现单一业务逻辑
领域服务主要实现多实体之间的复杂业务逻辑
Xxx 聚合
某个高度业务内聚性的聚合,里面包含聚合根、实体、值对象、领域服务等领域模型中的领域对象
entity 领域对象
主要包含聚合根、实体、值对象
aggregate 聚合根
主要包含聚合根实体,负责管理聚合内所有实体的生命周期,例如初始化,持久化等等
entity 实体
主要包含实体,依附于聚合根实体的生命周期,但是有唯一标识,一般需要落库
valobj 值对象
主要包含值对象,没有唯一标识,一般跟着聚合根落库,是聚合根对应表的部分字段,没有实际的对应表结构
repository 仓储
一个聚合一个仓储,实现聚合数据的持久化。领域服务通过仓储接口来访问基础资源,由仓储实现完成数据持久化和初始化。仓储一般包含:仓储接口和仓储实现
po 持久化对象
与表一一对应的java对象
mapper
使用mybatis-plus框架,每个PO对象都用对应的Mapper对象
service
使用mybatis-plus框架,每个PO对象都用对应的Service对象,其实单用mapper也可以,但是生成这个对象可以使用很多已经实现的数据持久化功能,可以提高开发效率
facade 仓储接口
为了解耦业务逻辑和基础资源,在基础层和领域层之间增加一层仓储服务,实现依赖倒置。通过这一层可以实现业务逻辑和基础层资源的依赖分离。在变更基础层数据库的时候,只要替换仓储实现就可以了,上层核心业务逻辑不会受基础资源变更的影响,从而实现依赖倒置。
但是用于为了方便领域内的不同聚合之前拆分,把仓储实现也放到了领域层里面
主要是实现数据持久化的统一接口,一般只包含save,findById,remove 3个方法,对应的是聚合根的持久化,从内存恢复聚合根,或者删除等等
由于业务常常包含更新操作,如果每次都通过save进行更新操作,会产生不必要的字段更新,或者导致数据不一致,所以特地再加上update方法,用于聚合根的更新持久化
除了上述的4个方法,可能还会有其他的查询方法,而这个查询方法不是面向接口调用,而是其他领域服务需要用的查询方法
persistence 仓储实现
对仓储接口的实现,主要基于上面所说的 mybatis-plus 的 mapper,service 去实现
infrastructure 基础层
基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。但是由于上面说的,为了方便领域模型的聚合拆分,也可以把仓储实现放到领域层
基础层包含基础服务,它采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。
util 工具
代码中需要用到的工具类
constant 常量
代码中需要用到的常量
读模型代码结构
├─DrugSystemQueryApplication.java
├─interfaces
| ├─facade
| | └UserController.java
├─infrastructure
| ├─utils
| | └DefaultUtil.java
| ├─enums
| | └Default.java
├─application
| ├─service
| | └UserQueryApplicationServiceImpl.java
| ├─repository
| | ├─service
| | | └UserServiceImpl.java
| | ├─po
| | | └UserPO.java
| | ├─persistence
| | | └UserRepositoryImpl.java
| | ├─mapper
| | | └UserMapper.java
| | ├─facade
| | | └UserRepository.java
读写模型主要差异
- 读模型代码结构删掉了领域层
- 仓储统一放到应用层
- 其余于写模型基本一直
问题汇总
-
在DDD的原则里,repository操作的都是聚合根,repository的作用就是把内存中的聚合根持久化,或者把持久化的数据还原为内存中的聚合根。repository中一般也只有getById,save,remove几个方法。例如取消订单的场景,我其实只需要更新order的状态等少数几个字段,但是如果调用repository的save方法,就会把订单其他字段以及订单明细数据都更新一次,这样就会造成性能影响,以及数据冲突的问题。
在repository增加只更新部分字段的方法,就是上文我们说的update方法,虽然这样会对repository有一定的污染,但是实现比较简单
-
消费mq的逻辑应该属于那一层?
消息订阅方一般在应用层监听和接受事件数据
-
订单父单和子单设计成一个聚合好,还是2个聚合好?
一般来说订单和订单明细是在一个聚合里面的
-
聚合根与领域服务在职责上有些重叠了,在实现的时候如何选择?
理论上,聚合根方法和领域服务都可以组合多个实体对象完成复杂的领域逻辑。但为了避免聚合根的业务逻辑过于复杂,避免聚合根类代码量过于庞大,建议聚合根除了承担它的聚合管理职能外,只作为实体实现与聚合根自身行为相关的业务逻辑。而将跨多个实体的复杂领域逻辑放在领域服务中实现。简单聚合的跨实体领域逻辑,可以考虑在聚合根方法中实现
-
仓储为什么不是放在基础层?
理论上仓储都应该放到基础层的。将它们放到领域层的聚合目录下主要是基于以下考虑,一个聚合会有一个仓储,以后微服务的演进基本上会以聚合为单位进行重组或者拆分,所以在代码拆分的时候以聚合目录进行整体迁移就可以了,这样在代码上不会花太多的时间
-
复杂的查询怎么办?
复杂的查询不建议放在领域层去解决。可以采用CQRS,也就是常说的读写分离的模式,或者直接在应用层完成复杂查询。本次我们实现的就是读写分离模式。
-
领域事件怎么实现?
同一个项目的微服务内不同聚合之间采用事件总线方式(SpringEvent),不同项目的微服务之间采用消息队列模式
-
事务控制放在哪里合适?
聚合内可以在领域服务采用事务机制,聚合之间或微服务之间可以在应用服务采用事务机制
-
是不是每个微服务都要有自己单独的数据库?
没有特殊考虑的话,建议一个微服务一个数据库,但是由于目前我们不动数据库,所以还是共用一个数据库,但是代码层面要把他当成不同数据库来对待,不同微服务的数据访问,一定要通过不同微服务所暴露的服务来获取,不可以直接操作其他微服务的数据表
-
使用Dubbo应该将哪一层的服务进行封装api
个人认为应该将应用层服务进行api封装,不同微服务需要互相访问的时候,导入相应的api包即可,里面包含了req和resp等入参和出参的对象
-
想问下在工厂里可以使用MapStruct来简化PO和Entity的数据初始化和持久化的转换吗
这个工具应该可以用来做DO和PO的转换,不过本次我们没用使用
-
复杂查询放在哪里?
由于查询一般没有太多的业务逻辑和规则控制,所以一般复杂的查询都会放在应用层
-
层间调用关系
依赖原则是外层依赖内层。例如不能从领域层去调应用层的服务