1. 背景
DDD 向来以高门槛而文明,他内部提出了非常多且抽象晦涩难懂的概念,比如实体、值对象、领域服务、领域事件、聚合根、工厂、仓库、应用服务等,第一批涌入人员很多被这些概念击退,少数坚持下来爱好学习的人继续往深处走,迎接他们的是更多的概念,比如 CQRS、六边形架构的内六边形&外六边形、输入适配器、输出适配器、防腐层、开放主机……
少数异常坚韧者熬了无数次通宵,终于将这些概念搞明白。一边感慨设计的精妙,可以形成科学且高度结构化的解决方案。一边又在摇头,叹息落地实现的难度,自己融会贯通已经这么艰难,还怎么奢求整个团队能步调一致。
追求到手真的是一场空吗?
这个问题在很长一段时间内一直困扰着我,按高度结构化进行开发,成本太高,很多关键的类和扩展点自己都没有办法记牢。不按结构化进行开发,代码就会失控,各种逻辑耦合在一起,几千甚至上万行代码的方法慢慢涌现,项目逐渐走向失控。
直到我将 结构化、标准化、模版化 这三个概念放在一起考虑,才真正豁然开朗。
-
结构化:DDD、CQRS 给出的解决方案都是高度结构化的方案,每个组件的边界和职责清晰明了,组件间的交互关系都有明确的规则。
-
标准化:在结构化的基础上,每个组件都具备高度的标准化。尽管承载的业务流程不同,但每个组件的设计都遵循同样的规则。
-
模版化:标准化意味着会有大量重复逻辑(不是重复代码),这些重复逻辑在开发手里就是 复制-粘帖-稍作修改。
既然是重复工作那就应该由机器完成!
2. 目的
目的非常明确:
-
降低概念的记忆成本,让初级开发不在惧怕 DDD。
-
统一规范,业务流程、组件设计在团队内保存高度一致。
-
提升开发效率,逻辑重复部分交由机器完成,提升开发效率。
3. 功能介绍
3.1. Maven 脚手架
Maven 脚手架主要实现整个项目的结构高度统一,降低新项目构建成本。
使用以下命令,可快速创建符合公司规范的项目:
mvn archetype:generate
-DarchetypeGroupId=com.geekhalo.lego
-DarchetypeArtifactId=services-archetype
-DarchetypeVersion=0.1.39-plugin_demo-SNAPSHOT
-DgroupId=com.geekhalo -DartifactId=user
-Dversion=0.1.39-plugin_demo-SNAPSHOT
该命令行是基于 Maven 的构建工具,用于生成项目骨架。具体来说,这个命令执行的是 archetype 插件的 generate
目标,用于创建一个预定义项目结构的模板实例。
参数解释如下:
-
-DarchetypeGroupId=com.geekhalo.lego
: 指定要使用的原型(archetype)所在的组ID,这是一个自定义的 Maven 组织标识符,对应于提供项目的骨架模板的组织或团体。 -
-DarchetypeArtifactId=services-archetype
: 指定要使用的原型的工件ID,这是特定于该组织下用于生成新项目的模板名称。 -
-DarchetypeVersion=0.1.39-plugin_demo-SNAPSHOT
: 设置所用原型版本,这里是一个快照版本号,表明它可能在开发过程中频繁更新。 -
-DgroupId=com.geekhalo
: 为将要生成的新项目设置组ID,这是新项目所属的Maven组织标识符。 -
-DartifactId=user
: 设置新项目的工件ID,即新项目的名字。 -
-Dversion=0.1.39-plugin_demo-SNAPSHOT
: 设置新项目的初始版本号,与原型版本保持一致,同样使用了一个快照版本。
综上所述,这条命令的作用是在本地通过Maven生成一个新的项目结构,该项目是基于 com.geekhalo.lego
组下的 services-archetype
模板,并且初始化时设置的项目信息是 groupId=com.geekhalo
,artifactId=user
,以及 version=0.1.39-plugin_demo-SNAPSHOT
。
命令执行完成后,你便可以看到新增符合公司规范的 user 模块:

这个项目是一个用户服务,它被划分为多个模块:
-
user-domain
:包含了用户服务的核心领域模型和领域逻辑。它是六边形架构中的核心层,不直接依赖于外部系统或技术栈。 -
user-infrastructure
:包含了用户服务的具体实现细节和技术栈相关的代码。例如,数据库访问层、消息队列客户端等。 -
user-app
:实现了用户服务的应用逻辑。它依赖于其他模块(如user-domain
、user-infrastructure
)来完成业务功能。 -
user-api
:定义了用户服务对外提供的接口,也就是服务契约,包括 基于Feign的RPC契约 和 基于RocketMQ的消息契约。 -
user-feign-service
:如果用户服务需要提供给其他微服务调用,那么这里会定义 Feign 服务接口。 -
user-feign-client
:是用户服务对外提供的 SDK,它提供了对用户服务的简单易用的 API 接口,使得其他服务能够快速地集成和调用用户服务的功能。 -
user-bootstrap
:启动用户服务的引导程序。它通常包含 Spring Boot 的启动类和一些配置文件。
在根目录下,有两个重要的文件:
-
pom.xml
:Maven 项目的配置文件,用于管理所有子模块的依赖关系和构建过程。 -
README.md
:项目的说明文档,通常包括项目简介、如何运行、如何开发等内容。
结构没有对错,不同的公司存在不同规范,这块不是关注的重点。
3.2. 自定义 Idea 插件
有了项目骨架后,接下来的重点就是业务开发。通过自定义 idea 插件,可以将规范融合到插件内,保障规范落地的同时,大幅降低开发成本。
3.2.1. 创建聚合根和视图模型
聚合根是DDD中最为重要的一个概念,也是承接业务逻辑的最小单元。
日常开发基本都是围绕聚合根进行的,对此 idea 很多功能都是围绕聚合根进行构建。
让我们使用插件新建一个聚合根 “BasicUser” 用于存储用户的基本信息。
在 domain 模块下的 basic 包上点击右键,选择 lego 菜单下的 “创建 聚合根” 功能,具体如下:

在弹出的对话框中填入 聚合根类名为“BasicUser”,其他保存默认,详见:

点击左上角的 View,切换到视图模型配置:

点击 “OK” 按钮,观察项目变化:
Domain 模块:

Infrastructure 模块:

App 模块:

创建一个简单的 BasicUser 聚合根,插件为我们生成一系列文件。这些文件之前都需手工完成,浪费了大量时间。
从设计上,项目采用 CQRS 进行设计,需要同时应对简单和复杂两个场景。换个说法就是:
-
业务通常从一个简单场景开始,需要满足快速开发的要求。
-
随着迭代的增加复杂性也随之增加,此时可以在不影响架构设计的前提下快速升级架构。
对于简单场景,推荐使用共享存储的 CQRS 架构,具体如下:

如果写操作和读操作两者差距巨大,推荐使用标准的 CQRS 架构,具体如下:

简单介绍完背景后,看框架帮我们生成了什么:
-
基于 DDD 的写流程
-
BasicUser:这是用户服务的核心领域对象,它包含了用户的属性信息以及与之相关的业务逻辑。作为领域聚合根,它可以确保数据的一致性和完整性,并且控制了对用户状态的修改操作。
-
AbstractBasicUserEvent:一个抽象事件,表示用户服务中发生的一些重要事件。这些事件可能会触发用户服务的状态变化或者引发其他行为。具体的事件类型可以通过继承该抽象类来实现。
-
BasicUserCommandRepository:这是用户服务的命令存储库,负责处理对用户对象的创建、更新和删除等操作。它通常会将这些操作持久化到数据库或其他存储介质中。
-
JpaBasedBasicUserCommandRepository:是用户服务的命令侧存储库的一个具体实现,它使用 JPA(Java Persistence API)来操作数据库。这种实现方式可以让开发者更专注于业务逻辑,而无需关心底层的数据访问细节。
-
BasicUserCommandApplication:这是用户服务的命令侧应用服务,它封装了用户服务的业务逻辑,并协调各个组件(如领域对象、存储库等)的工作。它接收来自客户端的请求,执行相应的业务逻辑,并返回结果。
-
基于 View 的读流程
-
BasicUserView:是用户服务的查询侧视图模型,它包含了用户的基本信息,但不会包含任何业务逻辑。它的主要作用是在查询时提供快速响应的服务,以满足高并发读取的需求。
-
BasicUserQueryRepository:这是用户服务的查询侧存储库,负责处理对用户视图模型的查询操作。它可以从数据库、缓存或者其他快速的数据源中获取数据。
-
JpaBasedBasicUserQueryRepository:这是用户服务的查询侧存储库的一个具体实现,它使用 JPA(Java Persistence API)来操作数据库。这种实现方式可以让开发者更专注于业务逻辑,而无需关心底层的数据访问细节。
-
BasicUserQueryApplication:这是用户服务的查询应用服务,它封装了用户服务的查询逻辑,并协调各个组件(如视图模型、存储库等)的工作。它接收来自客户端的查询请求,执行相应的查询逻辑,并返回结果。
idea 插件帮我们完成了大部分工作,快速生成共享存储的 CQRS 业务代码骨架。
3.2.2. 创建聚合根方法
有了聚合根后,我们需要在聚合根上添加业务方法。
在 BasicUser 类上右键选择 lego 下的 创建聚合根方法,如图所示:

在弹窗中填入方法名为:create,如图所示:

点击“OK”按钮,会有如下变化:



自动生成信息如下:
-
新创建的 create 目录,包含流程中的核心组件
-
CreateBasicUserCommand:这是一个创建用户的命令对象,它包含了创建用户所需的所有参数和信息。当客户端想要创建一个新的用户时,它会发送这个命令对象到用户服务。
-
CreateBasicUserContext:这是一个上下文对象,它包含了创建用户所需的环境信息和其他辅助信息,可以帮助操作流程更好地维护和共享数据。
-
BasicUserCreatedEvent:这是一个事件对象,表示用户已经被成功创建。当用户服务接收到 CreateBasicUserCommand 命令并成功创建了一个新的用户后,它会发布这个事件对象。
-
BasicUser 新增 create 业务方法
-
静态的 create 方法,用于使用 Context 对象创建 BasicUser
-
实例的 init 方法,用于对 BasicUser 进行核心状态设置、创建并发布领域事件
-
BasicUserCommandApplication 新增 create 方法
-
输入 CreateBasicUserCommand 对象,协调内部组件,完成创建流程
无需编写应用服务的代码,框架会自动生成 Proxy 类来完成全部流程。开发人员只需将精力放在 create 目录下的各种组件即可,组件的协作集成全部由 lego 框架完成。
此时,你就已经具备了一个不含任何业务逻辑的 创建用户 流程。
3.2.3. 创建查询方法
在 BasicUserQueryApplication 右键选择创建 “创建查询方法”,弹出如下对话框:

我们选择分页查询,确定后,自动生成:
@QueryServiceDefinition(
repositoryClass = BasicUserQueryRepository.class,
domainClass = BasicUserView.class
)
@Validated
public interface BasicUserQueryApplication {
// 分页查询方法
Page<BasicUserView> pageOf(@Valid PageByStatus query);
}
打开 PageByStatus 类,完善信息如下:
@NoArgsConstructor
@Data
public class PageByStatus{
// 使用 status 字段进行过滤
@FieldEqualTo("status")
private UserStatus status;
// 分页信息
private Pageable pageable;
// 排序信息
private Sort sort;
}
此时什么都不需要做,你便拥有了一个检索接口。
其中 PageByStatus 为 QueryObject,通过 注解 和 特定类型的字段声明查询能力:
-
@FieldEqualTo(“status”) 标明该字段将用于与 status 进行过滤
-
Pageable 类型的字段为分页信息,可以指定页码和每页大小,完成快速分页
-
Sort 类型的字段提供排序信息,用于对数据进行排序
3.2.4. 其他支持
除了生成骨架代码外,框架对核心组件也提供了支持。
3.2.4.1. 通用枚举
选择创建枚举,弹出如下对话框:

点击确定,自动生成枚举和Jpa 转化器,如果使用 MyBatis 也可以选择生成 TypeHandler。
UserStatus 代码如下:
public enum UserStatus implements CommonEnum {
;
// code 为枚举的唯一标识
private final int code;
// 枚举描述信息
private final String descr;
UserStatus(int code, String descr){
this.code = code;
this.descr = descr;
}
@Override
public int getCode() {
return this.code;
}
@Override
public String getDescription() {
return this.descr;
}
}
UserStatusConverter 代码如下:
// 基于枚举的 Code 完成持久化处理
@Converter(autoApply = true)
public class UserStatusConverter
extends CommonEnumAttributeConverter<UserStatus> {
public UserStatusConverter(){
super(UserStatus.values());
}
}
3.2.4.2. 上下文工厂
选择创建上下文工厂,弹出以下弹窗:

点击“OK” 自动生成 CreateBasicUserContextFactory 如下:
@Component
@Slf4j
public class CreateBasicUserContextFactory
extends AbstractSmartContextFactory<CreateBasicUserCommand, CreateBasicUserContext> {
public CreateBasicUserContextFactory(){
super(CreateBasicUserCommand.class, CreateBasicUserContext.class);
}
@Override
public CreateBasicUserContext create(CreateBasicUserCommand cmd) {
CreateBasicUserContext context = CreateBasicUserContext.apply(cmd);
return context;
}
}
3.2.4.3. 聚合根工厂
选择创建聚合根工厂,弹出以下弹窗:

点击“确定”自动生成 BasicUserFactory 如下:
@Component
@Slf4j
public class BasicUserFactory
extends AbstractSmartAggFactory<CreateBasicUserContext, BasicUser> {
public BasicUserFactory(){
super(CreateBasicUserContext.class, BasicUser.class);
}
@Override
public BasicUser create(CreateBasicUserContext context) {
return BasicUser.create(context);
}
}
3.2.4.4. 业务验证器
右键选择“创建业务验证器”,输入类名为:CreateBasicUserPhoneValidator,自动生成代码如下:
@Component
@Slf4j
public class CreateBasicUserPhoneValidator
extends AbstractBusinessValidator<CreateBasicUserContext> {
public CreateBasicUserPhoneValidator(){
super(CreateBasicUserContext.class);
}
@Override
public void validate(CreateBasicUserContext context, ValidateErrorHandler validateErrorHandler) {
log.info("validate(context) {}", context);
}
}
3.2.4.5. 结果转换器
右键选择“创建结果转换器”,弹出如下对话框:

自动生成代码如下:
@Component
@Slf4j
public class BasicUserKeyConverter
extends AbstractSmartResultConverter<BasicUser, CreateBasicUserContext, BasicUserKey> {
public BasicUserKeyConverter(){
super(BasicUser.class, CreateBasicUserContext.class, BasicUserKey.class);
}
@Override
public BasicUserKey convert(BasicUser agg, CreateBasicUserContext context) {
// 添加转换代码
return null;
}
}
4. 工程&源码
最后给出工程地址:https://gitee.com/litao851025/lego
后端专属技术群 构建高质量的技术交流社群,欢迎从事编程开发、技术招聘HR进群,也欢迎大家分享自己公司的内推信息,相互帮助,一起进步!
文明发言,以
交流技术
、职位内推
、行业探讨
为主广告人士勿入,切勿轻信私聊,防止被骗
加我好友,拉你进群
原文始发于微信公众号(Java知音):为了DDD 熬夜撸了一套 IDEA 插件
暂无评论内容