【DDD】DDD分层架构与传统三层架构的区别
# 【DDD】DDD 分层架构与传统三层架构的区别
# 文章目录
# DDD 分层与传统三层区别
根据 DDD 领域驱动设计原则,对应的软件架构也需要做出相应的调整。
我们常用的三层架构模型划分为表现层,业务逻辑层,数据访问层等,在 DDD
分层结构中既有联系又有区别,
个人认为主要有如下异同:
- 在架构设计上,在
DDD
分层结构中将传统三层架构的业务逻辑层拆解为应用层和领域层
其中 Application 划分为很薄的一层服务,非核心的逻辑放到此层去实现,核心的业务逻辑表现下沉到领域层去实现,凝练为更为精确的业务规则集合,通过领域对象去阐述说明。
- 在建模方式上,
DDD
分层的建模思维方式有别于传统三层
传统三层通常是以数据库为起点进行数据库分析设计,而DDD
则需要以业务领域模型为核心建模(即面向对象建模方式),更能体现对现实世界的抽象。
故在 DDD 分层凸显领域层的重要作用,领域层为系统的核心,包括所有的业务领域模型的抽象表达。 - 在职责划分上,基础设施层涵盖了 2 方面内容
- 持久化功能,其中原三层架构的数据访问层下沉到基础设施层的持久化机制实现
- 通用技术支持,一些公共通用技术支持也放到基础设施层去实现。
# DDD 分层详解
# 四层架构图
在该架构中,上层模块可以调用下层模块,反之不行。即
Interface
——>application
|domain
|infrastructure
application
——>domain
|infrastructure
domain
——>infrastructure
# 分层作用
分层 | 英文 | 描述 |
---|---|---|
表现层 | User Interface |
用户界面层,或者表现层,负责向用户显示解释用户命令 |
应用层 | Application Layer |
定义软件要完成的任务,并且指挥协调领域对象进行不同的操作。该层不包含业务领域知识。 |
领域层 | Domain Layer |
或称为模型层,系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手 |
基础设施层 | Infrastructure Layer |
主要有 2 方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;一是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现; |
# 领域对象
根据战术设计,关注的领域对象主要包括有
类型 | 英文 | 描述 |
---|---|---|
值对象 | value object |
无唯一标识的简单对象 |
实体 | entity |
充血的领域模型,有唯一标识 |
聚合(聚合根) | aggregate |
实体的聚合,拥有聚合根,可为某一个实体 |
领域服务 | service |
无法归类到某个具体领域模型的行为 |
领域事件 | event |
不常用 |
仓储 | repository |
持久化相关,与基础设施层关联 |
工厂 | factory |
负责复杂对象创建 |
模块 | module |
子模块引入,可以理解为子域划分 |
# DDD 编码实践(改进分层)
本文在对上述的传统四层的实践中,(1)根据 依赖倒置原则
对分层结构进行了改进,通过改变不同层的依赖关系(即将基础设施层倒置)来改进具体实现与抽象之间关系;(2)在基础设施层中增加 引用适配层
(防腐层)来增强防御策略,用来统一封装外部系统接口的引用。改进的分层结构如下:
依赖倒置原则(DIP):
- 高层模块不依赖于低层模块,两者都依赖于抽象;
- 抽象不应该依赖于细节,细节应依赖抽象
# 代码结构描述
eg.
后端 Java 代码工程为例,
表现层
在此代码结构中表现为 api层
,对外暴露接口的最上层
1 | ├─com.company.microservice |
其中在上述目录结构中,Domain 层中为对 module
进行划分,实际上默认该层只有一个模块,根据微服务划分可以进行增加模块来规范代码结构。
示例代码工程:
GITHUB 地址:https://github.com/smingjie/bbq-ddd.git
# 扩展定义注解和接口声明
(1) 自定义注解
:在使用 DDD 中自定义了标记的注解 (@DDDAnnotation) 和其衍生子注解,分别是
@DomainAggregate
@DomainAggregateRoot
@DomainEntity
@DomainValueObject
@DomainService
@DomainRepository
@DomainEvent
@ApplicationService
@DomainAssembler
@DomainConverter
等注解,详见代码的 infrastructure.general.extension.ddd.annotation.**;其中有些注解继承了 spring 的 @Component
, 将会自动注册为 spring bean,有些注解为了标记用于后续扩展;
引入了 Assembler 装配器 / Converter 转换器,通过组合模式解耦继承关系,在 api 层和持久化层都有相应的实现。
(2) 自定义接口
:在 domain.common 定义了部分通用的 契约
接口,如领域对象元数据获取接口 IDomainMetaData
,通过接口解耦继承关系。其他还有: IDomainSaveOrUpdate
IDomainDelete
...
等 Command
# 领域模型注入仓储类的问题
区别于传统的分层后,在 domain 中更多关注业务逻辑,考虑到要与 spring 框架集成,需要注意一个领域模型中注入仓储类的问题
在传统分层中,controller,service,repo 均注册为 spring 管理的 bean,
但是在 domain 层中,service 一部分的业务逻辑划分到了具体的领域对象中去实现了,显然这些对象却不能注册为单例 bean,
因此在此处不能沿用与原来分层结构中 service 层中通过@Autowired
or@Resource
等注入仓储接口,
关于这个问题,此处建议使用 ApplicationContext
实现
即通过一个工具类
ApplicationContextUtils
实现ApplicationContextAware
获取 bean 的方法,即getBean()
方法,
然后我们就可以在我们的领域模型中直接应用该工具类来获取 Spring 托管的 singleton 对象,即
xxxRepo=ApplicationContextUtils.getBean (“xxxRepository”)
1 |
|
考虑到代码结构简洁性,还可以封装一层仓储工厂,只用来获取相应的仓储 Bean
。
1 | /** |
然后在领域模型中就可以直接调用该工厂方法来获取仓储接口的实现,
比如 DictRepo
为定义的仓储接口, DictDao
为该接口的准实现类
1 | //直接指定实现 |
# 一些个人思考…
上述经典四层架构,笔者更愿意理解为 DDD 在编码实现阶段的一个体现或应用。
补充一点:DDD 除了在编码实践阶段,还体现在需求分析、设计阶段等过程,DDD 推荐不割裂系统的需求和设计,我们这里可以合并称作系统建模过程,可参考 DDD - 建模过程分析一文,不再赘述。
当然除了这个经典四层架构模型,DDD 还有五层架构、六边形架构等,所以这里抛出一个问题,
# 项目按上述经典四层架构进行搭建,可以说是 DDD 架构实践么?
关于这个问题,笔者想引入一对哲学概念,哲学有言形式与内容,现象与本质等辩证关系(当然与本文可能也没啥太大关系啦);从这两个角度来阐述本人的观点:
-
形式与内容:经典四层架构是一个 DDD 实现的形式,相当于给我们提供了一个框框来让我们自己去实现;在这个框框里面我们怎么实现是自由发挥的,但也是有约束的,这个约束体现在 DDD 对每一层的作用的约定,如每个层约定做了什么功能,充当什么角色等。尤其是对 Domain 层的约定,才是最重要的。那么我们按照哲学辩证的套话来说,形式上满足了 DDD 架构,但这应该是片面的,具体还要看内容,即具体实现是怎样的。
-
现象与本质:接着上述观点,如果要看实现,就要具体分析一下现象与本质嘞。上面笔者也有提到,DDD 除了四层经典架构,还有五层架构(包括其演化的多层架构)、六边形架构等也都是 DDD 提供的架构模型(形式),那这些都可以理解 DDD 架构模式的外显形式,那么又有哪些共性呢?可自行查询,本文直接给结论,即
它们都有 Domain 层,Domain 层,Domain 层
(重要的事情说三遍~~,该结论 DDD 作者译著有写到…),所以不管架构模式怎么演化,Domain 是核心不能变。
那么如上分析,我们在回到这个问题,我们是不是可以给出一个这样的答案:
形式上符合 DDD 架构,具体是不是 DDD 的架构实践,本质上还要看
- (1)项目是否包括有 Domain 层;
- (2)Domain 层是否满足 DDD 战术篇的要求(或者可暂时简单理解为充血模型吧)
# 题外话:Spring 与 DDD
- Spring 框架中,Spring 为我们提供了
@Service
@Repository
等注解,为我们分离行为和行为(注册为 Bean)和属性(数据模型),同时通过@Autowired
在合适地方进行注入行为,因为行为被注册为 Spring 容器中的 Bean 后,减少了频繁创建行为的开销,只有属性的数据模型作为数据的载体来传递数据。提供很大的便捷性。但也阻碍了我们应用 DDD 编码实践, Spring 框架主张分离,DDD 思想主张合并,我们在 Spring 框架中使用 DDD 则需要在其基础上进行一些权衡取舍,即 如何将注册为 Bean 的行为穿插到原有的贫血模型中来构建充血模型是我们要解决的问题 - 关于这个问题,笔者使用了 Spring 框架提供的获取容器内已经注册的 Bean 接口,直接调用接口,在有属性的领域模型中来获取行为;主要还是体现融入领域模型中的部分 Service 获取仓储接口来实现持久化过程。
当然,上述的说明都是从一个软件开发人员的角度来阐述说明 DDD 在编码实践阶段的应用 。
除此之外在业务领域的建模分析过程中也可引入该概念。
比如我们现在所倡导的微服务化,如何划分或拆分微服务;如何有效地区分限界上下文,划分子域;如何构建一个有效的聚合,识别聚合根等。。。
- 附在最后的,关于笔者对于 DDD 在需求设计阶段应用的学习总结
- DDD - 建模分析过程