为了DDD 熬夜撸了一套 IDEA 插件)
👉 这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:
《项目实战(视频)》:从书中学,往事上“练” 《互联网高频面试题》:面朝简历学习,春暖花开 《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题 《精进 Java 学习指南》:系统学习,互联网主流技术栈 《必读 Java 源码专栏》:知其然,知其所以然
👉这是一个或许对你有用的开源项目
国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。
功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、CRM 等等功能:
Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud 视频教程:https://doc.iocoder.cn 【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本
1. 背景
DDD 向来以高门槛而文明,他内部提出了非常多且抽象晦涩难懂的概念,比如实体、值对象、领域服务、领域事件、聚合根、工厂、仓库、应用服务等,第一批涌入人员很多被这些概念击退,少数坚持下来爱好学习的人继续往深处走,迎接他们的是更多的概念,比如 CQRS、六边形架构的内六边形&外六边形、输入适配器、输出适配器、防腐层、开放主机……
少数异常坚韧者熬了无数次通宵,终于将这些概念搞明白。一边感慨设计的精妙,可以形成科学且高度结构化的解决方案。一边又在摇头,叹息落地实现的难度,自己融会贯通已经这么艰难,还怎么奢求整个团队能步调一致。
追求到手真的是一场空吗?
这个问题在很长一段时间内一直困扰着我,按高度结构化进行开发,成本太高,很多关键的类和扩展点自己都没有办法记牢。不按结构化进行开发,代码就会失控,各种逻辑耦合在一起,几千甚至上万行代码的方法慢慢涌现,项目逐渐走向失控。
直到我将 结构化、标准化、模版化 这三个概念放在一起考虑,才真正豁然开朗。
结构化:DDD、CQRS 给出的解决方案都是高度结构化的方案,每个组件的边界和职责清晰明了,组件间的交互关系都有明确的规则。 标准化:在结构化的基础上,每个组件都具备高度的标准化。尽管承载的业务流程不同,但每个组件的设计都遵循同样的规则。 模版化:标准化意味着会有大量重复逻辑(不是重复代码),这些重复逻辑在开发手里就是 复制-粘帖-稍作修改。
既然是重复工作那就应该由机器完成!
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro 视频教程:https://doc.iocoder.cn/video/
2. 目的
目的非常明确:
降低概念的记忆成本,让初级开发不在惧怕 DDD。 统一规范,业务流程、组件设计在团队内保存高度一致。 提升开发效率,逻辑重复部分交由机器完成,提升开发效率。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud 视频教程:https://doc.iocoder.cn/video/
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
欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,“长按”或“扫描”下方二维码噢:
星球的内容包括:项目实战、面试招聘、源码解析、学习路线。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
微信扫码关注该文公众号作者