乐趣区

关于ddd:实践篇手把手教你落地DDD-京东云技术团队

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 第三步、保护畛域对象生命周期

在上一步中,loadDomainsaveDomain 这两个办法还没有失去探讨,这两个办法跟畛域对象的生命周期非亲非故。

对于畛域对象的生命周期的具体常识,读者能够自行学习理解。

不论是 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 等知识点。

作者:京东物流 覃玉杰

内容起源:京东云开发者社区

退出移动版