您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
手把手教你落地DDD
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
手把手教你落地DDD
教室****
2023-05-26
IP归属:北京
18040浏览
# 1. 前言 常见的DDD实现架构有很多种,如经典四层架构、六边形(适配器端口)架构、整洁架构(Clean Architecture)、CQRS架构等。架构无优劣高下之分,只要熟练掌握就都是合适的架构。本文不会逐个去讲解这些架构,感兴趣的读者可以自行去了解。 本文将带领大家从日常的三层架构出发,精炼推导出我们自己的应用架构,并且将这个应用架构实现为Maven Archetype,最后使用我们Archetype创建一个简单的CMS项目作为本文的落地案例。 需要明确的是,本文只是给读者介绍了DDD应用架构,还有许多概念没有涉及,例如实体、值对象、聚合、领域事件等,如果读者对完整落地DDD感兴趣,可以到本文最后了解更多。 # 2. 应用架构演化 我们很多项目是基于三层架构的,其结构如图: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-11-14YBFHfI9yZQKE323.png) 我们说三层架构,为什么还画了一层 Model 呢?因为 Model 只是简单的 Java Bean,里面只有数据库表对应的属性,有的应用会将其单独拎出来作为一个 Maven Module,但实际上可以合并到 DAO 层。 接下来我们开始对这个三层架构进行抽象精炼。 ## 2.1 第一步、数据模型与DAO层合并 为什么数据模型要与DAO层合并呢? 首先,数据模型是贫血模型,数据模型中不包含业务逻辑,只作为装载模型属性的容器; 其次,数据模型与数据库表结构的字段是一一对应的,数据模型最主要的应用场景就是DAO层用来进行 ORM,给 Service 层返回封装好的数据模型,供Service 获取模型属性以执行业务; 最后,数据模型的 Class 或者属性字段上,通常带有 ORM 框架的一些注解,跟DAO层联系非常紧密,可以认为数据模型就是DAO层拿来查询或者持久化数据的,数据模型脱离了DAO层,意义不大。 ## 2.2 第二步、Service层抽取业务逻辑 下面是一个常见的 Service 方法的伪代码,既有缓存、数据库的调用,也有实际的业务逻辑,整体过于臃肿,要进行单元测试更是无从下手。 ```java 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层的方法经过抽取之后,将得到如下的伪代码: ```java 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方法非常直观,就是获取模型、执行业务逻辑、保存模型,再协调基础设施完成其余的操作。 抽取完领域模型后,我们工程的结构如下图: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-11-14uWn7iIpd9djnQLh.png) ## 2.3 第三步、维护领域对象生命周期 在上一步中,`loadDomain`、`saveDomain` 这两个方法还没有得到讨论,这两个方法跟领域对象的生命周期息息相关。 关于领域对象的生命周期的详细知识,读者可以自行学习了解。 不管是 loadDomain 还是 saveDomain,我们一般都要依赖于数据库,所以这两个方法对应的逻辑,肯定是要跟 DAO 产生联系的。 保存或者加载领域模型,我们可以抽象成一种组件,通过这种组件进行封装模型加载、保存的操作,这种组件就是Repository。 注意,Repository 是对加载或者保存领域模型(这里指的是聚合根,因为只有聚合根才会有Repository)的抽象,必须对上层屏蔽领域模型持久化的细节,因此其方法的入参或者出参,一定是基本数据类型或者领域模型,不能是数据库表对应的数据模型。 以下是 Repository 的伪代码: ```java 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。 现在,我们项目的架构图是这样的了: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-11-17dh990ilCzl0p24Ay.png) > 由于数据模型属于贫血模型,自身没有业务逻辑,并且只有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` 层。 经过第四步的抽象,其架构图为: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-11-18JweHoYao24Xn9w5w.png) ## 2.5 第五步、完整的包结构 我们继续对第四步中出现的包进行整理,此时还需要考虑一个问题,我们的启动类应该放在哪里? 由于有很多的 User Interface,所以启动类放在任意一个User Interface中都不合适,放置在Application Service中也不合适,因此,启动类应该存放在单独的模块中。又因为 application这个名字被应用层占用了,所以将启动类所在的模块命名为 launcher,一个项目可以存在多个launcher,按需引用User Interface。 加入启动包,我们就得到了完整的 maven 包结构。 包结构如图所示: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-11-199wSveWHEpoWu0wp.png) 至此,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 克隆项目 将项目克隆到本地: ```shell git clone https://github.com/feiniaojin/ddd-archetype.git ``` 直接使用主分支即可,然后使用IDEA打开该项目 ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-11-29299dgNnCSCvIBM24n.png) ### 3.2.4 archetype:create-from-project 配置打开IDEA的`run/debug configurations`窗口,如下: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-13-26ac10GlitVdcJbeZ.png) 选择`add new configurations`,弹出以下窗口: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-13-27MqgcjpF6SvaV8LQ.png) 其中,上图中1~4各个标识的值为: 标识`1` - 选择"+"号; 标识`2` - 选择"Maven"; 标识`3` - 命令为: ```shell archetype:create-from-project -Darchetype.properties=archetype.properties ``` > 注意,在IDEA中添加的命令默认不需要加mvn 标识`4` - 选择`ddd-archetype`的根目录 以上配置完成后,点击执行该命令。 ### 3.2.5 install 上一步执行完成且无报错之后,配置`install`命令。 ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-13-27wV8yZ9yFxhcFWZn.png) 其中,上图中1~2各个标识的值为: 标识`1` - 值为`install`; 标识`2` - 值为上一步运行的结果,路径为: ``` ddd-archetype/target/generated-sources/archetype ``` `install`配置完成之后,点击执行。 ### 3.2.6 archetype:crawl `install`执行完成且无报错,接着配置`archetype:crawl`命令。 ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-13-27nKzxHQtB86Ra35UV.png) 其中,标识1中的值为: ```shell archetype:crawl ``` 配置完成,点击执行即可。 ## 3.3 使用ddd-archetype初始化项目 - 创建项目时,点击`manage catalogs`: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-13-39N40GJwL0eDvfJJIR.png) - 将本地的maven私服中的`archetype-catalog.xml`加入到catalogs中: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-13-39pVSJNgQ39LwjpBOb.png) 添加成功,如下: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-13-40VPFgQRn1jDCVIzW.png) - 创建项目时,选择本地archetype-catalog,并且选择`ddd-archetype`,填入项目信息并创建项目: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-13-406MKvOiNmWmXB6vv.png) - 项目创建完成后: ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-24-13-40EOiZCUsmBY24fexx.png) # 4. 代码案例 本文提供了配套的代码案例,该案例使用DDD和本文的应用架构实现了简单的CMS系统。案例项目采用前后端分离的方式,因此有后端和前端两个代码库。 ## 4.1 后端 后端项目使用本文的`ddd-archetype`创建,实现了部分CMS的功能,并落地部分DDD的概念。 GitHub链接:https://github.com/feiniaojin/ddd-example-cms ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-25-13-41NkFxjrSsr0xcOpw.png) 实现的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 ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-25-13-41R1uNkCQxhVw41JO41.png) ## 4.3 运行截图 ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-25-13-41VtTzBAdgAWS25Otb.png) # 5. 总结以及进一步学习 本文通过对贫血三层架构进行精炼,推导出适合我们落地的应用架构,并且将之实现为Maven Archetype以应用到实际开发,然而应用架构只是落地DDD的一个知识点,要完整落地DDD还必须体系化地掌握限界上下文、上下文映射、充血模型、实体、值对象、领域服务、Factory、Repository等知识点。 作者将多年领域驱动设计的经验,撰写成《悟道DDD(Thinking in DDD)》这一电子书,力求用最平实、最通俗、最容易理解的方式将DDD分享给各位朋友。 本电子书永久免费阅读,欢迎沟通交流。 ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-05-25-11-28nVq0QzwtN28CZ5rw.png) 项目地址:https://github.com/feiniaojin/Thinking-in-DDD 在线阅读:http://ddd.feiniaojin.com/
上一篇:京东小程序接入ARVR的技术方案和性能调优
下一篇:Tomcat处理http请求之源码分析
教室****
文章数
5
阅读量
2434
作者其他文章
01
一站式统一返回值封装、异常处理、异常错误码解决方案—最强的Sping Boot接口优雅响应处理器
1. 简介Graceful Response是一个Spring Boot体系下的优雅响应处理器,提供一站式统一返回值封装、异常处理、异常错误码等功能。使用Graceful Response进行web接口开发不仅可以节省大量的时间,还可以提高代码质量,使代码逻辑更清晰。强烈推荐你花3分钟学会它!Graceful Response的Github地址: https://github.com/feinia
01
还在自己实现责任链?我建议你造轮子之前先看看这个开源项目
1. pie 简介责任链模式是开发过程中常用的一种设计模式,在SpringMVC、Netty等许多框架中均有实现。我们日常的开发中如果要使用责任链模式,通常需要自己来实现,但自己临时实现的责任链既不通用,也很容易产生框架与业务代码耦合不清的问题,增加Code Review 的成本。Netty的代码向来以优雅著称,早年我在阅读Netty的源码时,萌生出将其责任链的实现应用到业务开发中的想法,之后花了
01
手把手教你落地DDD
1. 前言常见的DDD实现架构有很多种,如经典四层架构、六边形(适配器端口)架构、整洁架构(Clean Architecture)、CQRS架构等。架构无优劣高下之分,只要熟练掌握就都是合适的架构。本文不会逐个去讲解这些架构,感兴趣的读者可以自行去了解。本文将带领大家从日常的三层架构出发,精炼推导出我们自己的应用架构,并且将这个应用架构实现为Maven Archetype,最后使用我们Arche
01
手把手教你实战TDD
1. 前言领域驱动设计,测试驱动开发。我们在《手把手教你落地DDD》一文中介绍了领域驱动设计(DDD)的落地实战,本文将对测试驱动开发(TDD)进行探讨,主要内容有:TDD基本理解、TDD常见误区、TDD技术选型,以及案例实战。希望通过本文,读者能够理解掌握TDD并将其应用于实际开发中。2. TDD基本理解测试驱动开发(TDD)是一种软件开发方法,要求开发者在编写代码之前先编写测试用例,然后编写代
教室****
文章数
5
阅读量
2434
作者其他文章
01
一站式统一返回值封装、异常处理、异常错误码解决方案—最强的Sping Boot接口优雅响应处理器
01
还在自己实现责任链?我建议你造轮子之前先看看这个开源项目
01
手把手教你实战TDD
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号