1. 前言
常见的 DDD 实现架构有很多种,如经典四层架构、六边形(适配器端口)架构、整洁架构(Clean Architecture)、CQRS 架构等。架构无优劣高下之分,只有熟练掌握就都是适合的架构。本文不会一一去解说这些架构,感兴趣的读者能够自行去理解。
本文将率领大家从日常的三层架构登程,精炼推导出咱们本人的利用架构,并且将这个利用架构实现为 Maven Archetype,最初应用咱们 Archetype 创立一个简略的 CMS 我的项目作为本文的落地案例。
须要明确的是,本文只是给读者介绍了 DDD 利用架构,还有许多概念没有波及,例如实体、值对象、聚合、畛域事件等,如果读者对残缺落地 DDD 感兴趣,能够到本文最初理解更多。
2. 利用架构演变
咱们很多我的项目是基于三层架构的,其构造如图:
咱们说三层架构,为什么还画了一层 Model 呢?因为 Model 只是简略的 Java Bean,外面只有数据库表对应的属性,有的利用会将其独自拎进去作为一个 \
Maven Module,但实际上能够合并到 DAO 层。
接下来咱们开始对这个三层架构进行形象精炼。
2.1 第一步、数据模型与 DAO 层合并
为什么数据模型要与 DAO 层合并呢?
首先,数据模型是贫血模型,数据模型中不蕴含业务逻辑,只作为装载模型属性的容器;
其次,数据模型与数据库表构造的字段是一一对应的,数据模型最次要的利用场景就是 DAO 层用来进行 ORM,给 Service 层返回封装好的数据模型,供 Service 获取模型属性以执行业务;
最初,数据模型的 Class 或者属性字段上,通常带有 ORM 框架的一些注解,跟 DAO 层分割十分严密,能够认为数据模型就是 DAO 层拿来查问或者长久化数据的,数据模型脱离了 DAO 层,意义不大。
2.2 第二步、Service 层抽取业务逻辑
上面是一个常见的 Service 办法的伪代码,既有缓存、数据库的调用,也有理论的业务逻辑,整体过于臃肿,要进行单元测试更是无从下手。
public class Service {
@Transactional
public void bizLogic(Param param) {checkParam(param);// 校验不通过则抛出自定义的运行时异样
Data data = new Data();// 或者是 mapper.queryOne(param);
data.setId(param.getId());
if (condition1 == true) {biz1 = biz1(param.getProperty1());
data.setProperty1(biz1);
} else {biz1 = biz11(param.getProperty1());
data.setProperty1(biz1);
}
if (condition2 == true) {biz2 = biz2(param.getProperty2());
data.setProperty2(biz2);
} else {biz2 = biz22(param.getProperty2());
data.setProperty2(biz2);
}
// 省略一堆 set 办法
mapper.updateXXXById(data);
}
}
这是典型的事务脚本的代码:先做参数校验,而后通过 biz1、biz2 等子办法做业务,并将其后果通过一堆 Set 办法设置到数据模型中,再将数据模型更新到数据库。
因为所有的业务逻辑都在 Service 办法中,造成 Service 办法十分臃肿,Service 须要理解所有的业务规定,并且要分明如何将基础设施串起来。同样的一条规定,例如 if(condition1=true),很有可能在每个办法外面都呈现。
业余的事件就该让业余的人干,既然业务逻辑是跟具体的业务场景相干的,咱们想方法把业务逻辑提取进去,造成一个模型,让这个模型的对象去执行具体的业务逻辑。这样 Service 办法就不必再关怀外面的 if/else 业务规定,只须要通过业务模型执行业务逻辑,并提供基础设施实现用例即可。
将业务逻辑形象成模型,这样的模型就是畛域模型。
要操作畛域模型,必须先取得畛域模型,但此时咱们先不论畛域模型怎么失去,假如是通过 loadDomain
办法取得的。通过 Service 办法的入参,咱们调用 loadDomain
办法失去一个模型,咱们让这个模型去做业务逻辑,最初执行的后果也都在模型里,咱们再将模型回写数据库。当然,怎么写数据库的咱们也先不论,假如是通过 saveDomain
办法。
Service 层的办法通过抽取之后,将失去如下的伪代码:
public class Service {public void bizLogic(Param param) {
// 如果校验不通过,则抛一个运行时异样
checkParam(param);
// 加载模型
Domain domain = loadDomain(param);
// 调用内部服务取值
SomeValue someValue=this.getSomeValueFromOtherService(param.getProperty2());
// 模型本人去做业务逻辑,Service 不关怀模型外部的业务规定
domain.doBusinessLogic(param.getProperty1(), someValue);
// 保留模型
saveDomain(domain);
}
}
依据代码,咱们曾经将业务逻辑抽取进去了,畛域相干的业务规定关闭在畛域模型外部。此时 Service 办法十分直观,就是获取模型、执行业务逻辑、保留模型,再协调基础设施实现其余的操作。
抽取完畛域模型后,咱们工程的构造如下图:
2.3 第三步、保护畛域对象生命周期
在上一步中,loadDomain
、saveDomain
这两个办法还没有失去探讨,这两个办法跟畛域对象的生命周期非亲非故。
对于畛域对象的生命周期的具体常识,读者能够自行学习理解。
不论是 loadDomain 还是 saveDomain,咱们个别都要依赖于数据库,所以这两个办法对应的逻辑,必定是要跟 DAO 产生分割的。
保留或者加载畛域模型,咱们能够形象成一种组件,通过这种组件进行封装模型加载、保留的操作,这种组件就是 Repository。
留神,Repository 是对加载或者保留畛域模型(这里指的是聚合根,因为只有聚合根才会有 Repository)的形象,必须对下层屏蔽畛域模型长久化的细节,因而其办法的入参或者出参,肯定是根本数据类型或者畛域模型,不能是数据库表对应的数据模型。
以下是 Repository 的伪代码:
public interface DomainRepository {void save(AggregateRoot root);
AggregateRoot load(EntityId id);
}
接下来咱们要思考在哪里实现DomainRepository
。既然 DomainRepository 与底层数据库有关联,然而咱们当初 DAO 层并没有引入 Domain 这个包,DAO 层天然无奈提供 DomainRepository 的实现,咱们初步思考是不是能够将 DomainRepository 实现在 Service 层。
然而,如果咱们在 Service 中实现 DomainRepository,势必须要在 Service 层操作数据模型:查问进去数据模型再封装为畛域模型、或者将畛域模型转为数据模型再通过 ORM 保留,这个过程不该是 Service 层关怀的。
因而,咱们决定在 DAO 层间接引入 Domain 包,并在 DAO 层提供 DomainRepository 接口的实现,DAO 层查问出数据模型之后,封装成畛域模型供 DomainRepository 返回。
这样调整之后,DAO 层不再向 Service 返回数据模型,而是返回畛域模型,这就暗藏了数据库交互的细节,咱们也把 DAO 层换个名字称之为 Repository。
当初,咱们我的项目的架构图是这样的了:
因为数据模型属于贫血模型,本身没有业务逻辑,并且只有 Repository 这个包会用到,因而咱们将之合并到 Repository 中,接下来不再独自列举。
2.4 第四步、泛化形象
在第三步中,咱们的架构图曾经跟经典四层架构十分类似了,咱们再对某些层进行泛化形象。
- Infrastructure
Repository 仓储层其实属于基础设施层,只不过其职责是长久化和加载聚合,所以,咱们将 Repository 层改名为 infrastructure-persistence
,能够了解为基础设施层长久化包。
之所以采取这种 infrastructure-XXX 的格局进行命名,是因为 Infrastructure 可能会有很多的包,别离提供不同的基础设施反对。
例如:个别的我的项目,还有可能须要引入缓存,咱们就能够再加一个包,名字叫infrastructure-cache
。
对于内部的调用,DDD 中有防腐层的概念,将内部模型通过防腐层进行隔离,防止净化本地上下文的畛域模型。咱们应用入口(Gateway)来封装对外部零碎或资源的拜访(具体见《企业应用架构模式》,18.1 入口(Gateway)),因而将对外调用这一层称之为infrastructure-gateway
。
留神:Infrastructure 层的门面接口都应先在 Domain 层定义,其办法的入参、出参,都应该是畛域模型(实体、值对象)或者根本类型。
- User Interface
Controller 层其实就是用户接口层,即 User Interface 层,咱们在我的项目简称 ui。当然了可能很多开发者会感觉叫 UI 如同很顺当,认为 UI 就是 UI 设计师设计的图形界面。
Controller 层的名字有很多,有的叫 Rest,有的叫 Resource,思考到咱们这一层不只是有 Rest 接口,还可能还有一系列 Web 相干的拦截器,所以我个别称之为 Web。因而,咱们将其改名为 ui-web,即用户接口层的 Web 包。
同样,咱们可能会有很多的用户接口,然而他们通过不同的协定对外提供服务,因此被划分到不同的包中。
咱们如果有对外提供的 RPC 服务,那么其服务实现类所在的包就能够命名为 ui-provider
。
有时候引入某个中间件会同时减少 Infrastructure 和 User Interface。
例如,如果引入 Kafka 就须要考虑一下,如果是给 Service 层提供调用的,例如逻辑执行完发送音讯告诉上游,那么咱们再加一个包infrastructure-publisher
;如果是生产 Kafka 的音讯,而后调用 Service 层执行业务逻辑的,那么就能够命名为 ui-subscriber
。
- Application
至此,Service 层目前曾经没有业务逻辑了,业务逻辑都在 Domain 层去执行了,Service 只是协调畛域模型、基础设施层实现业务逻辑。
所以,咱们把 Service 层改名为 Application Service
层。
通过第四步的形象,其架构图为:
2.5 第五步、残缺的包构造
咱们持续对第四步中呈现的包进行整顿,此时还须要思考一个问题,咱们的启动类应该放在哪里?
因为有很多的 User Interface,所以启动类放在任意一个 User Interface 中都不适合,搁置在 Application Service 中也不适合,因而,启动类应该寄存在独自的模块中。又因为 application 这个名字被应用层占用了,所以将启动类所在的模块命名为 launcher,一个我的项目能够存在多个 launcher,按需援用 User Interface。
退出启动包,咱们就失去了残缺的 maven 包构造。
包构造如图所示:
至此,DDD 我的项目的整体构造根本讲完了。
2.6 精炼后的思考
在通过后面五步精炼失去这个架构图中,经典四层架构的四层都呈现了,而且长得跟六边形架构也很像。这是为什么呢?
其实,不论是经典四层架构、还是六边形架构,亦或者整洁架构,都是对系统利用的形容,兴许形容的侧重点不一样,然而形容的是同一个事物。既然形容的是同一个事物,长得像才是天经地义的,不可能只是换一个形容形式,零碎就从根本上产生了扭转。
对于任何一个利用,都能够看成“输出 - 解决 - 输入”的过程。
“输出”环节:通过某种协定对外裸露畛域的能力,这些协定可能是 REST、可能是 RPC、可能是 MQ 的订阅者,也可能是 WebSocket,也可能是一些任务调度的 Task;
”解决“环节:解决环节是整个利用的外围,代表了利用具备的外围能力,是利用的价值所在,利用在这个环节执行业务逻辑,贫血模型由 Service 执行业务解决,充血模型则是由模型进行业务解决。
“输入”环节,业务逻辑执行实现之后将后果输入到内部。
不论咱们采纳的什么架构,其形容的利用的外围都是这个过程,不用生吞活剥非得用什么利用架构。
正如《金刚经》所言:所有有为法,如梦幻泡影,如露亦如电,应作如是观;凡所有相,皆是虚妄;若见诸相非相,即见如来。
3. ddd-archetype
3.1 Maven Archetype 介绍
Maven Archetype 是一个 Maven 插件,能够帮忙开发人员疾速创立我的项目的根底构造,大大减少开发人员在创立我的项目时所需的工夫和精力,并且能够确保我的项目构造的一致性和可重用性,从而进步代码品质和可维护性。
咱们在介绍 DDD 利用架构时,对我的项目的构造进行了介绍。咱们将我的项目分为多个 Maven Module,如果每个我的项目都手工创立一次,是比拟繁琐的工作,也不利我的项目构造的对立。
咱们应用 Maven Archetype 创立 DDD 我的项目初始化的脚手架,使其在初始化时残缺实现上文第五步的利用架构。
3.2 ddd-archetype 的应用
3.2.1 我的项目介绍
ddd-archetype 是一个 Maven Archetype 的原型工程,咱们将其克隆到本地之后,能够装置为 Maven Archetype,帮忙咱们疾速创立 DDD 我的项目脚手架。
我的项目链接:
https://github.com/feiniaojin/ddd-archetype
3.2.2 装置过程
以下将以 IDEA 为例展现 ddd-archetype 的装置应用过程,次要过程是:
克隆我的项目
–>archetype:create-from-project
–>install
–>archetype:crawl
3.2.3 克隆我的项目
将我的项目克隆到本地:
git clone https://github.com/feiniaojin/ddd-archetype.git
间接应用主分支即可,而后应用 IDEA 关上该我的项目
3.2.4 archetype:create-from-project
配置关上 IDEA 的 run/debug configurations
窗口,如下:\
抉择add new configurations
,弹出以下窗口:
其中,上图中 1~4 各个标识的值为:
标识1
– 抉择 ”+” 号;
标识2
– 抉择 ”Maven”;
标识3
– 命令为:
archetype:create-from-project -Darchetype.properties=archetype.properties
留神,在 IDEA 中增加的命令默认不须要加 mvn
标识 4
– 抉择ddd-archetype
的根目录
以上配置实现后,点击执行该命令。
3.2.5 install
上一步执行实现且无报错之后,配置 install
命令。
其中,上图中 1~2 各个标识的值为:
标识1
– 值为install
;
标识2
– 值为上一步运行的后果,门路为:
ddd-archetype/target/generated-sources/archetype
install
配置实现之后,点击执行。
3.2.6 archetype:crawl
install
执行实现且无报错,接着配置 archetype:crawl
命令。
其中,标识 1 中的值为:
archetype:crawl
配置实现,点击执行即可。
3.3 应用 ddd-archetype 初始化我的项目
- 创立我的项目时,点击
manage catalogs
:\ - 将本地的 maven 私服中的
archetype-catalog.xml
退出到 catalogs 中:
增加胜利,如下:\
- 创立我的项目时,抉择本地 archetype-catalog,并且抉择
ddd-archetype
,填入我的项目信息并创立我的项目:\ - 我的项目创立实现后:\
4. 代码案例
本文提供了配套的代码案例,该案例应用 DDD 和本文的利用架构实现了简略的 CMS 零碎。案例我的项目采纳前后端拆散的形式,因而有后端和前端两个代码库。
4.1 后端
后端我的项目应用本文的 ddd-archetype
创立,实现了局部 CMS 的性能,并落地局部 DDD 的概念。
GitHub 链接:https://github.com/feiniaojin/ddd-example-cms
实现的 DDD 概念有:实体、值对象、聚合根、Factory、Repository、CQRS。
技术栈:
- Spring Boot
- H2 内存数据库
- Spring Data JDBC
无内部中间件依赖,clone 到本地即可编译运行,十分不便。
4.2 前端
前端我的项目基于 vue-element-admin
开发,具体装置形式见代码库的 README。
GitHub 链接:https://github.com/feiniaojin/ddd-example-cms-front
4.3 运行截图
5. 总结以及进一步学习
本文通过对贫血三层架构进行精炼,推导出适宜咱们落地的利用架构,并且将之实现为 Maven Archetype 以利用到理论开发,然而利用架构只是落地 DDD 的一个知识点,要残缺落地 DDD 还必须体系化地把握限界上下文、上下文映射、充血模型、实体、值对象、畛域服务、Factory、Repository 等知识点。
作者:京东物流 覃玉杰
内容起源:京东云开发者社区