赞
踩
根据DDD领域驱动设计原则,对应的软件架构也需要做出相应的调整。 我们常用的三层架构模型划分为表现层,业务逻辑层,数据访问层等,在DDD
分层结构中既有联系又有区别, 个人认为主要有如下异同:
DDD
分层结构中将传统三层架构的业务逻辑层拆解为应用层和领域层 其中Application划分为很薄的一层服务,非核心的逻辑放到此层去实现,核心的业务逻辑表现下沉到领域层去实现,凝练为更为精确的业务规则集合,通过领域对象去阐述说明。;
DDD
分层的建模思维方式有别于传统三层 传统三层通常是以数据库为起点进行数据库分析设计,而DDD
则需要以业务领域模型为核心建模(即面向对象建模方式),更能体现对现实世界的抽象。 故在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 | 子模块引入,可以理解为子域划分 |
eg.
后端Java代码工程为例, 表现层
在此代码结构中表现为api层
,对外暴露接口的最上层
- ├─com.company.microservice
- │ │
- │ ├─apis API接口层
- │ │ ├─model 视图模型,数据模型定义 vo/dto(大多数情況是一样的)
- │ │ ├─assembler 装配器,实现模型转换eg. apiModel<=> domainModel
- │ │ └─controller 控制器,对外提供(Restful)接口
- │ │
- │ ├─application 应用层
- │ │ ├─service 应用服务,非核心服务
- │ │ ├─task 任务定义,协调领域模型
- │ │ └─*** others
- │ │
- │ ├─domain 领域层
- │ │ ├─common 公共代码抽取,限于领域层有效
- │ │ ├─events 领域事件
- │ │ ├─model 领域模型
- │ │ │ ├─dict 领域划分的模块,可理解为子域划分
- │ │ │ │ ├─DictVo.java 领域值对象
- │ │ │ │ ├─DictEntity.java 领域实体,充血的领域模型,如本身的CRUD操作在此处
- │ │ │ │ ├─DictAgg.java 领域聚合,通常表现为实体的聚合,需要有聚合根
- │ │ │ │ └─DictService.java 领域服务,不能归与上述模型,如分页条件查询等可写在此处
- │ │ │ ├─xxx
- │ │ │ │ ├─xxxEntity.java
- │ │ │ │ ├─bbbAgg.java
- │ │ │ │ └─cccAgg.java
- │ │ ├─service 领域服务类,一些不能归属某个具体领域模型的行为
- │ │ └─factory 工厂类,负责复杂领域对象创建,封装细节
- │ │
- │ ├─infrastructure 基础设施层
- │ │ ├─persistent 持久化机制
- │ │ │ ├─po 持久化对象
- │ │ │ └─repository 仓储类,持久化接口&实现,可与ORM映射框架结合
- │ │ ├─general 通用技术支持,向其他层输出通用服务
- │ │ │ ├─config 配置类
- │ │ │ ├─toolkit 工具类
- │ │ │ └─common 基础公共模块等
- │ │
- │ └─resources
- │ ├─statics 静态资源
- │ ├─template 系统页面
- │ └─application.yml 全局配置文件
其中在上述目录结构中,Domain层中为对module
进行划分,实际上默认该层只有一个模块,根据微服务划分可以进行增加模块来规范代码结构。
区别于传统的分层后,在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”)
- @Component
- public class ApplicationContextUtils implements ApplicationContextAware {
-
- public static ApplicationContext appctx;
-
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
- ApplicationContextUtil.appctx=applicationContext;
- }
-
- /**
- * @return ApplicationContext
- */
- public static ApplicationContext getApplicationContext() {
- return appctx;
- }
-
- /**
- * 获取对象
- *
- * @param name spring配置文件中配置的bean名或注解的名称
- * @return 一个以所给名字注册的bean的实例
- * @throws BeansException 抛出spring异常
- */
- public static <T> T getBean(String name) throws BeansException {
- return (T) appctx.getBean(name);
- }
-
- /**
- * 获取类型为requiredType的对象
- *
- * @param clazz 需要获取的bean的类型
- * @return 该类型的一个在ioc容器中的bean
- * @throws BeansException 抛出spring异常
- */
- public static <T> T getBean(Class<T> clazz) throws BeansException {
- return appctx.getBean(clazz);
- }
-
- /**
- * 如果ioc容器中包含一个与所给名称匹配的bean定义,则返回true否则返回false
- *
- * @param name ioc容器中注册的bean名称
- * @return 存在返回true否则返回false
- */
- public static boolean containsBean(String name) {
- return appctx.containsBean(name);
- }
- }
考虑到代码结构简洁性,还可以封装一层仓储工厂,只用来获取相应的仓储Bean
。
- /**
- * 简化版的仓储工厂--用来统一获取仓储的实现Bean
- *
- * @author jockeys
- * @date 2020/9/12
- */
- public class RepoFactory {
-
- /**
- * 根据仓储接口类型获取对应实现且默认取值第一个
- *
- * @param tClass 具体目标类型
- * @param <T> 仓储接口类型
- * @return 如果不是指定实现,默认获得第一个实现Bean
- */
- public static <T> T get(Class<? extends T> tClass) {
-
- Map<String, ? extends T> map = ApplicationUtils.getApplicationContext().getBeansOfType(tClass);
- Collection<? extends T> collection = map.values();
- if (collection.isEmpty()) {
- throw new PersistException("未找到仓储接口或其指定的实现:" + tClass.getSimpleName() );
- }
- return collection.stream().findFirst().get();
- }
- }
然后在领域模型中就可以直接调用该工厂方法来获取仓储接口的实现, 比如DictRepo
为定义的仓储接口,DictDao
为该接口的准实现类
- //直接指定实现
- DictRepo repo= RepoFactory.get(DictDao.class);
- //不指定实现取Spring容器中默认注册第1位的Bean
- DictRepo repo= RepoFactory.get(DictRepo.class);
上述经典四层架构,笔者更愿意理解为DDD在编码实现阶段的一个体现或应用。
补充一点:DDD除了在编码实践阶段,还体现在需求分析、设计阶段等过程,DDD推荐不割裂系统的需求和设计,我们这里可以合并称作系统建模过程
当然除了这个经典四层架构模型,DDD还有五层架构、六边形架构等,所以这里抛出一个问题,
关于这个问题,笔者想引入一对哲学概念,哲学有言形式与内容,现象与本质等辩证关系(当然与本文可能也没啥太大关系啦);从这两个角度来阐述本人的观点:
@Service
@Repository
等注解,为我们分离行为和行为(注册为Bean)和属性(数据模型),同时通过@Autowired
在合适地方进行注入行为,因为行为被注册为Spring容器中的Bean后,减少了频繁创建行为的开销,只有属性的数据模型作为数据的载体来传递数据。提供很大的便捷性。但也阻碍了我们应用DDD编码实践, Spring框架主张分离,DDD思想主张合并,我们在Spring框架中使用DDD则需要在其基础上进行一些权衡取舍,即 如何将注册为Bean的行为穿插到原有的贫血模型中来构建充血模型是我们要解决的问题
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/404921
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。