为了DDD 熬夜撸了一套Idea插件(工程源码)

架构的小事2024-05-17 11:46:12  88

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 模块:

image

这个项目是一个用户服务,它被划分为多个模块:

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 菜单下的 “创建 聚合根” 功能,具体如下:

image

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

image

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

image

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

Domain 模块:

image

Infrastructure 模块:

image

App 模块:

image

创建一个简单的 BasicUser 聚合根,插件为我们生成一系列文件。这些文件之前都需手工完成,浪费了大量时间。

从设计上,项目采用 CQRS 进行设计,需要同时应对简单和复杂两个场景。换个说法就是:

业务通常从一个简单场景开始,需要满足快速开发的要求。

随着迭代的增加复杂性也随之增加,此时可以在不影响架构设计的前提下快速升级架构。

对于简单场景,推荐使用共享存储的 CQRS 架构,具体如下:

image

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

image

简单介绍完背景后,看框架帮我们生成了什么:

基于 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 下的 创建聚合根方法,如图所示:

image

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

image

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

image

image

image

自动生成信息如下:

新创建的 create 目录,包含流程中的核心组件

CreateBasicUserCommand:这是一个创建用户的命令对象,它包含了创建用户所需的所有参数和信息。当客户端想要创建一个新的用户时,它会发送这个命令对象到用户服务。

CreateBasicUserContext:这是一个上下文对象,它包含了创建用户所需的环境信息和其他辅助信息,可以帮助操作流程更好地维护和共享数据。

BasicUserCreatedEvent:这是一个事件对象,表示用户已经被成功创建。当用户服务接收到 CreateBasicUserCommand 命令并成功创建了一个新的用户后,它会发布这个事件对象。

BasicUser 新增 create 业务方法

静态的 create 方法,用于使用 Context 对象创建 BasicUser

实例的 init 方法,用于对 BasicUser 进行核心状态设置、创建并发布领域事件

BasicUserCommandApplication 新增 create 方法

输入 CreateBasicUserCommand 对象,协调内部组件,完成创建流程

无需编写应用服务的代码,框架会自动生成 Proxy 类来完成全部流程。开发人员只需将精力放在 create 目录下的各种组件即可,组件的协作集成全部由 lego 框架完成。

此时,你就已经具备了一个不含任何业务逻辑的 创建用户 流程。

3.2.3. 创建查询方法

在 BasicUserQueryApplication 右键选择创建 “创建查询方法”,弹出如下对话框:

image

我们选择分页查询,确定后,自动生成:

@QueryServiceDefinition( repositoryClass = BasicUserQueryRepository.class, domainClass = BasicUserView.class)@Validatedpublic interface BasicUserQueryApplication { // 分页查询方法 Page pageOf(@Valid PageByStatus query);}

打开 PageByStatus 类,完善信息如下:

@NoArgsConstructor@Datapublic 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. 通用枚举

选择创建枚举,弹出如下对话框:

image

点击确定,自动生成枚举和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 { public UserStatusConverter{ super(UserStatus.values); }}

3.2.4.2. 上下文工厂

选择创建上下文工厂,弹出以下弹窗:

image

点击“OK” 自动生成 CreateBasicUserContextFactory 如下:

@Component@Slf4jpublic class CreateBasicUserContextFactory extends AbstractSmartContextFactory { public CreateBasicUserContextFactory{ super(CreateBasicUserCommand.class, CreateBasicUserContext.class); } @Override public CreateBasicUserContext create(CreateBasicUserCommand cmd) { CreateBasicUserContext context = CreateBasicUserContext.apply(cmd); return context; }}

3.2.4.3. 聚合根工厂

选择创建聚合根工厂,弹出以下弹窗:

image

点击“确定”自动生成 BasicUserFactory 如下:

@Component@Slf4jpublic class BasicUserFactory extends AbstractSmartAggFactory { public BasicUserFactory{ super(CreateBasicUserContext.class, BasicUser.class); } @Override public BasicUser create(CreateBasicUserContext context) { return BasicUser.create(context); }}

3.2.4.4. 业务验证器

右键选择“创建业务验证器”,输入类名为:CreateBasicUserPhoneValidator,自动生成代码如下:

@Component@Slf4jpublic class CreateBasicUserPhoneValidator extends AbstractBusinessValidator { public CreateBasicUserPhoneValidator{ super(CreateBasicUserContext.class); } @Override public void validate(CreateBasicUserContext context, ValidateErrorHandler validateErrorHandler) { log.info("validate(context) {}", context); }}

3.2.4.5. 结果转换器

右键选择“创建结果转换器”,弹出如下对话框:

image

自动生成代码如下:

@Component@Slf4jpublic class BasicUserKeyConverter extends AbstractSmartResultConverter { 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

转载此文是出于传递更多信息目的。若来源标注错误或侵犯了您的合法权益,请与本站联系,我们将及时更正、删除、谢谢。
https://www.414w.com/read/554651.html
0
随机主题
现货金价短期仍面临回调修正日本等了足足4年, 终于等来北京的高官, 开口就对中方提2个要求陈幸同混双击败张本美和, 陈熠击败林依诺, 赵大成击败薛飞奥迪A8L 55TFSI,买了半年多,感受跟你说争议!NBA最佳阵容引球迷不满,这仨人凭啥入选?米兰与斯图加特酝酿交易, 卡卢卢西米奇成筹码, 交换28岁德甲银靴反制说到做到, 中方拉长清单, 外交部宣布出手, 美国政客财路被断《乘风2024》宣布将直播, 被质疑投票数据注水, 节目组回应: 将对异常数据予以清除处理你不知道的冷知识——荷兰篇深圳形成“15分钟社康圈”, 人均期望寿命达83.93岁法国宣布试射空射核导弹爆笑漫画《阿U校园爆笑王》、免费奶茶、雪极星滑雪体验券, 橙柿福利爆款上新 | 橙柿福利夏威夷是如何变成美国的第五十个州的,美国的第一次干涉别国内政汽车工业进入新时代, 日产逍客开始大降价, 还值得入手吗?国内最大的硝盐储能新材料项目开工舒淇宝格丽之夜疑似被日本女星针对…冈田武史调侃: 浙江队主场氛围非常好, 我执教时为何没这待遇?后来者“卷”上, 荣威D5X DMH技术与性价比齐发力“美国口碑最佳AR眼镜”VITURE ONE体验:这是最懂御三家游戏玩家的品牌!谢晖死活不换人! 王禹李申圆跑拉胯了 贝里奇战犯级表现, 葛副总还是别上了英国政府意外宣布7月大选, 苏纳克目的何在?
最新回复(0)