DDD分层

为什么分层

引用《领域驱动设计模式、原理与实践》

为了避免将代码库变成大泥球(BBoM)并因此减弱领域模型的完整性且最终减弱可用性,系统架构要支持技术复杂性与领域复杂性的分离。引起技术实现发生变化的原因与引起领域逻辑发生变化的原因显然不同,这就导致基础设施和领域逻辑问题会以不同速率发生变化

每一层都有各自的职责,显然这也是符合SRP的

如何分层

DDD的标准形态

  1. User Interface是用户接口层,主要用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将信息传递给Application层的接口
  2. Application层是应用层,负责多进程管理及调度、多线程管理及调度、多协程调度和维护业务实例的状态模型。当调度层收到用户接口层的请求后,委托Context层与本次业务相关的上下文进行处理
  3. Domain层是领域层,定义领域模型,不仅包括领域对象及其之间关系的建模,还包括对象的角色role的显式建模
  4. Infrastructure层是基础实施层,为其他层提供通用的技术能力:业务平台,编程框架,持久化机制,消息机制,第三方库的封装,通用算法,等等

根据DDD细化业务逻辑层

模块

结合maven的module,项目中现在分了八个module

1
2
3
4
5
6
7
8
9
10
<modules>
<module>generator-assist-dao</module> <!-- 生成的dao -->
<module>generator-assist-client-api</module> <!-- swagger api yaml -->
<module>assist-client-api</module> <!-- 生成的api -->
<module>assist-controller</module> <!-- controller -->
<module>assist-service</module> <!-- domain -->
<module>assist-infrastructure</module> <!-- infrastructure -->
<module>assist-common</module> <!-- 基础common -->
<module>start</module> <!-- 启动入口及test -->
</modules>

start

入口模块

包结构:

  • start 只有一个启动类
  • test 单元测试

除了启动类,还有单元测试

generator-assist-dao

生成的dao类

包结构:

  • repository
    • model 与数据库对应的实体类
  • repository
    • mapper mybatis的mapper

现在实践落地时,这个模块是个空模块,why?

DDD中明确了repository概念,并属于domain层,但dao是对底层数据库的封装,具体实现类放在infrastructure层更合理

在COLA中,作者也是为了领域层的纯洁性,依赖反转了,repository接口定义在domain层,而实现在infra层

但在落地时,domain与infra出现了循环依赖,COLA把实现放在了app层,这样有些另类,所以暂时先把repository全部放在了service层

迷思:

1、基于mybatis的实现,mapper本身是接口,repository实现类放在domain层,不要接口,这样满足DDD分层规则,但离DIP差了一步

2、在《DDD之熵》中提过

DDD引入repository放在了领域层,一是对应聚合根的概念,二是抽象了数据库访问,,但DDD限界上下文可能不仅限于访问数据库,还可能访问同样属于外部设备的文件、网络与消息队列。为了隔离领域模型与外部设备,同样需要为它们定义抽象的出口端口,这些出口端口该放在哪里呢?如果依然放在领域层,就很难自圆其说。例如,出口端口EventPublisher支持将事件消息发布到消息队列,要将这样的接口放在领域层,就显得不伦不类了。倘若不放在位于内部核心的领域层,就只能放在领域层外部,这又违背了整洁架构思想

3、是不是有别的理论支撑解决问题2

generator-assist-client-api

为了生成api的swagger yaml文件

包结构:

  • swagger-spec
    • all swagger所有yaml文件的整合文件
    • apis swagger定义的api
    • models swagger定义的api中的model
  • swagger-templates 模板文件

assist-client-api

通过swagger生成的api接口与api中的model

包结构:

  • client
    • api swagger生成的api接口
    • model swagger生成的request,response对象

assist-controller

controller层,放置controller

包结构:

  • controller 所有的controller
  • xxljob xxljob补偿任务

按DDD分层规范,controller属于ui层,处理restful请求

  • 接受请求 —— 由spring提供能力
  • 请求格式校验及转换 —— 格式校验遵循java Validation规范
  • 权限校验 —— 由网关处理
  • 路由请求 —— 网关处理
  • 记录请求 —— 专门Accessfilter处理
  • 回复响应 —— 由spring提供能力

为什么还有一个xxljob包,从能力区分,xxljob放到infra层才对。这个原因类似generator-assist-dao模块,xxljob的handler需要调用application service,需要依赖service module

因此可以把xxljob作为远程请求的一个入口,与controller一样归在ui层

这儿引出一点思考,controller真的是ui层吗?能划分到别的层吗?

有几种设计思路

  1. ui层完全归属于大前端,不在后端,也就不在ddd中,后端都是从application service开始
  2. controller归于ui
  3. controller归于infra,controller毕竟是依赖具体底层框架能力的adapter

controller是基于springboot的具体实现

从上面的分析,可以看出controller逻辑上是归到infra层,但物理上不能放到infra模块;也不能简单把controller看作MVC中的C,还有很多像xxljob样的入口

  1. 入口会有很多,如controller、xxljob,还有mq等等
  2. 还有进程内的,如event,应用层,基础设施层,领域层都有event,怎么区分event是个问题
  3. application serivce与domain service区分也常常给人带来烦恼

这儿是否可以借鉴《DDD之形》中的端口和适配器架构

把controller看作driving adapter,既然区分这么复杂,那可不可以简单点,加厚controller,整合入口与application service

简单点分成两部分:远程服务与本地服务

  • 远程服务:定义会跨进程服务,分为资源(Resource)服务、供应者(Provider)服务、控制器(Controller)服务与事件订阅者(Event Subscriber)服务
  • 本地服务:所有远程服务需要调用领域服务,必须经过本地服务才能调用;明确隔离外界与领域,防止领域模型外泄

assist-service

domain层,但现在还是三层结构的思路,什么类都有,app service,domain service,dto,event 甚至还有基础设施层类

包结构

  • BO
  • builder
  • common
  • component
  • convertor
  • domain
  • dto
  • event
  • interceptor
  • listener
  • model
  • repository
  • service
  • thrid
  • valid

assist-infrastructure

基础设施层

包结构

  • config 配置信息
  • adapter 外部调用封装
    • clients 外部调用实现
    • pl 服务接口的契约 published language
  • dp domain primitive 这是不是应该在domain层
  • common 公共类,(InvoiceType与InvoiceTypeEnum的问题)
  • event
    • publish 事件发布者,此包为空,直接依赖spring不需要自实现了
  • exception 异常类
  • gateway 网关,封装访问外部系统或资源行为的对象
    • wechat 外部名称
      • api 外接接口
      • dto 外接接口dto
  • local
    • pl
  • ports
    • clients 外部调用接口
  • repository
    • model
  • resources 资源
  • service 依赖外部的service
  • util 工具类

现在的包结构很丰富,最常见的包就是gateway,配合acl与外部交互

U表示上游(Upstream)的被依赖方,D表示下游(Downstream)的依赖方。防腐层(ACL)放在下游,将上游的消息转化为下游的领域模型

结合generator-assist-dao模块的问题,是否可以扩大ACL,而不仅限于gateway中,像资源库一样,不必完全遵循DDD只抽象repository,像访问第三方应用,缓存,消息都可以抽象出来,契合端口履行的职责一样


改造

1
2
3
4
5
6
7
8
9
<modules>
<module>generator-assist-dao</module> <!-- 生成的dao -->
<module>generator-assist-client-api</module> <!-- swagger api yaml -->
<module>assist-client-api</module> <!-- 生成的api -->
<module>assist-ohs</module> <!-- ohs -->
<module>assist-service</module> <!-- domain -->
<module>assist-acl</module> <!-- acl -->
<module>start</module> <!-- 启动入口及test -->
</modules>

assist-controller

根据上面的分析,这一层可以更厚实些

改名为assist-ohs

OHS,open host service 开放主机服务,定义公开服务的协议,包括通信的方式、传递消息的格式(协议)

包结构

  • remote
    • controller
    • openapi
    • xxljob
    • subscribe
  • local
    • appservices
    • pl (plush language) request,response
    • convertor

assist-service

domain层

包结构

  • domain 领域对象
  • service 领域服务
  • factory 领域对象工厂
  • builder 领域对象构造器

assist-acl

扩大了基础设施层,隔离领域层与外部依赖,对所有外部环境一视同仁,无需针对资源库做特殊化处理,如此也可保证架构的简单性,repository、client、cahce…

领域层依赖port接口

包结构

  • config 配置信息
  • port 依赖外部接口
    • repository 数据库接口
    • client 第三方系统接口
    • publisher 消息接口
    • cache 缓存接口
  • adapter port的具体实现
    • repository
    • pl
    • client

总结

模块划分以及包结构还只是一家之言,一是有充足的理论体系支撑,不管按DDD标准,还是变形,更多地有理有据,与团队、也与自己达成一致;二是domain的抽象,一切都是为了领域模型的稳定性和扩展性,形只是表象

我们这个项目还是太注重了形,最重要的domain还是过弱

公众号:码农戏码
欢迎关注微信公众号『码农戏码』