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链接:github.com/feiniaojin/…
完结的DDD概念有:实体、值目标、聚合根、Factory、Repository、CQRS。
技术栈:
- Spring Boot
- H2内存数据库
- Spring Data JDBC
无外部中间件依赖 ,clone到本地即可编译运转,非常方便。
4.2 前端
前端项目基于vue-element-admin
开发,具体装置方法见代码库的README。
GitHub链接:github.com/feiniaojin/…
4.3 运转截图
5. 总结以及进一步学习
本文经过对贫血三层架构进行精粹,推导出合适咱们落地的运用架构,而且将之完结为Maven Archetype以运用到实际开发,然而运用架构只是落地DDD的一个常识点,要完好落地DDD还必须体系化地把握限界上下文、上下文映射、充血模型、实体、值目标、范畴服务、Factory、Repository等常识点。
作者:京东物流 覃玉杰
内容来源:京东云开发者社区