爲了DDD熬夜撸了一套Idea插件(工程源碼)

架構的小事 2024-05-18 04:45:20
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<BasicUserView> pageOf(@Valid PageByStatus query);}

打開 PageByStatus 類,完善信息如下:

@NoArgsConstructor@Datapublic 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 UserStatusConverter extends CommonEnumAttributeConverter<UserStatus> { public UserStatusConverter(){ super(UserStatus.values()); }}3.2.4.2. 上下文工廠

選擇創建上下文工廠,彈出以下彈窗:

image

點擊“OK” 自動生成 CreateBasicUserContextFactory 如下:

@Component@Slf4jpublic 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. 聚合根工廠

選擇創建聚合根工廠,彈出以下彈窗:

image

點擊“確定”自動生成 BasicUserFactory 如下:

@Component@Slf4jpublic 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@Slf4jpublic 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. 結果轉換器

右鍵選擇“創建結果轉換器”,彈出如下對話框:

image

自動生成代碼如下:

@Component@Slf4jpublic 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

0 阅读:4

架構的小事

簡介:感謝大家的關注