Domain Driven Design项目架构
Domain Driven Design 目录结构层级
TreeView
├── Application.java
├── assembler DTO 组装器:将领域对象组装成 DTO
├── config 配置文件目录
├── constant 常量、枚举目录
├── domain 领域层
│ ├── event 领域事件:MQ event
│ ├── model 领域对象
│ ├── repository 领域仓库
│ ├── service 业务领域服务
│ └── translator 翻译器:将持久化层 Entity 或 DTO 翻译为领域对象。
├── infrastructure 基础设施目录
│ ├── exception 异常
│ ├── interceptor 权限拦截
│ ├── repository 领域层仓库具体实现
│ │ ├── dao mysql、oracle、mongodb
│ │ │ └── impl
│ │ ├── mock 哑实现
│ │ │ └── impl
│ │ └── redis redis 缓存
│ │ └── impl
│ ├── runner 程序启动初始化 runner
│ ├── task 定时任务
│ ├── transport 第三方服务交互
│ ├── util 工具类
│ └── validation 校验层
├── service 应用服务层
└── ui 对外开放的用户层,User Interface
├── controller 控制器
└── dto 用于外部数据交互的 DTO
├── request
└── response
框架说明
User Interface 用户层
用户层,对外以各种协议提供服务,包含:
DTO
包括 request 和 response 两部分,通过它定义入参和出参的契约,在 DTO 层可以使用基础设施层的 validation 组件完成入参格式校验
Controller
Controller 使用基础设施层公共组件完成通用的工作:
- 通过注解 RequestMapping 添加 servlet 路由
- 通过 interceptor 完成登录权限/角色校验
- 简单的数据转换
Application Layer 应用层
Service
应用服务层,组合 domain 层的领域对象和基础设施层的公共组件,根据业务需要包装出多变的服务,以适应多变的业务服务需求。
应用服务层主要访问 domain 领域对象,完成服务逻辑的包装。
应用服务层也会访问基础设施层的公共组件,如 redis,完成领域消息的生产和获取等。
Assembler
组装器,负责将多个 domain 领域对象组装为需要的 DTO 对象,比如查询帖子列表,需要从Post(帖子)领域对象中获取帖子的详情,还需要从 User(用户)领域对象中获取用户的基本信息。
组装器中不应当有业务逻辑在里面,主要负责格式转换、字段映射等职责。
Domain Layer 领域层
业务领域层,是我们最应当关心的一层,也是最多变的一层,需要保证这一层是高内聚的。确保所有的业务逻辑都留在这一层,而不会遗漏到其他层。按照 DDD(domain driven design)理论,主要有如下概念构成:
Domain Entity
领域实体对象。有唯一标识(主键),可变的业务实体对象,有自己的生命周期。比如门店
就是一个业务实体,它需要有一个唯一性业务标识表征(门店 ID),同时它的状态(开店/停业)和内容(名称/地址)可以不断发生变化。
Domain Value Object
领域值对象。可以没有唯一性业务标识,且一旦定义,就不可变的,它通常是短暂的。这和Java中的值对象(基本类型和String类型)类似。比如门店领域中,门店的位置信息
可以理解为是一个值对象,不需要为门店的位置信息定义唯一标识,直接使用门店 ID
就可以找到每个门店的位置信息。同时,它具有省/市/区
和详细地址
几个属性,一旦任一个属性发生变化,则需要重建这个位置信息对象并赋值给门店
实体的引用。
Domain Factory
领域对象工厂。用于复杂领域对象的创建/重建。重建是指通过 respostory 获取到对象后,重建或组装多个对象为领域对象。
Domain Service
领域服务。区别于应用服务,它属于业务领域层。
可以认为,如果某种方法无法归类给任何单一实体/值对象,则就为这些方法建立相应的领域服务即可。比如:门店收银服务,需要操作门店/会员两个实体。
传统意义上的 util static 方法中,只要涉及到业务逻辑的部分,都可以考虑归入domain service。
Domain Event
领域事件。领域中产生的一些消息事件,通过事件通知/订阅的方式异步操作,可以在性能和解耦层面得到好处。
Repository
领域仓库。我们将仓库的接口定义归类在 Domain 层,因为它和 Domain entity 联系紧密。User Interface 和基础实施的持久化层交互,都需要实现领域仓库接口对应的增删改查操作。
仓库的实际实现根据不同的存储介质而不同,可以是redis、mysql等。
鉴于存储介质可以同时存在有多套:redis、mysql,memory cache, 且各个存储介质的字段属性名不一致,因此需要使用translator来做翻译,将持久化层的对象翻译为统一的领域对象。
Translator
翻译器。将持久化层的对象翻译为统一的领域对象。
翻译器中不应当有业务逻辑在里面,主要负责格式转换、字段映射等职责。
Infrastructure Layer 基础设施层
基础设施层提供公共功能组件,供 controller、service、domain 层调用。
Repository impl
对 domain 层 repository 接口的实现,对应每种存储介质有其特定实现,如 mysql、oracle的 dao 等等。repository impl 会调用 mybatis、jooq、redis client完成实际的存储层操作。
Interceptor
权限校验器,判定客户端是否有访问该资源的权限。提供给User Interface层的Controller调用。
Exception
异常分类及定义,同时提供公共的异常处理逻辑,具体由ExceptionHandler实现。
Transport 第三方服务交互层
transport 完成和第三方服务的交互,可以有多种协议形式的实现,如http+json、二进制文件流,用于对第三方服务的请求和响应进行适配,提供一个防腐层的作用。
注意要点
各个 package 的详细解释参考上节框架说明,着重注意如下几点:
-
domain.repository 包里面只有仓库的接口定义,实际的实现交给了 infrastructure 中的 repository module,该做法被称作
依赖倒置
,好处在于确保 domain 层语义完整,同时对确保业务领域的一致性也有帮助,再者可以在 domain 实现内存形式的 repository 哑实现,从而让 domain 可以真正脱离掉 infrastructure -
infrastructure.repository 作为仓库层,会将实体的增删改查操作委托给具体的存储层服务,如 mysql 对应的 dao 实现,还有 redis 的实现
-
少写应用层 Service,多把业务逻辑封装到 Domain
-
Controller 要瘦,仅处理消息或数据转发,Model 仅处理领域逻辑。而 Service 通常负责领域间的交互及某些预处理(比如数据验证、权限验证等等)
Reference
推荐阅读
-
Domain Driven Design项目架构
-
浅谈DDD(domain-driven design-领域驱动设计)
-
[译文]Domain Driven Design Reference(一)—— 前言
-
[译文]Domain Driven Design Reference(三)—— 模型驱动设计的构建模块
-
[译文]Domain Driven Design Reference(四)—— 柔性设计
-
[译文]Domain Driven Design Reference(五)—— 为战略设计的上下文映射
-
[译文]Domain Driven Design Reference(三)—— 模型驱动设计的构建模块
-
[译文]Domain Driven Design Reference(六)—— 提炼战略设计
-
[译文]Domain Driven Design Reference(四)—— 柔性设计
-
荐 DDD(Domain-Driven Design)领域驱动架构介绍.md