手把手教你落地DDD

来源:https://www.cnblogs.com/88223100/archive/2023/07/23/Hands-teach-you-how-to-land-DDD.html
-Advertisement-
Play Games

一、前言 常見的DDD實現架構有很多種,如經典四層架構、六邊形(適配器埠)架構、整潔架構(Clean Architecture)、CQRS架構等。架構無優劣高下之分,只要熟練掌握就都是合適的架構。本文不會逐個去講解這些架構,感興趣的讀者可以自行去瞭解。 本文將帶領大家從日常的三層架構出發,精煉推導 ...


一、前言
常見的DDD實現架構有很多種,如經典四層架構、六邊形(適配器埠)架構、整潔架構(Clean Architecture)、CQRS架構等。架構無優劣高下之分,只要熟練掌握就都是合適的架構。本文不會逐個去講解這些架構,感興趣的讀者可以自行去瞭解。
本文將帶領大家從日常的三層架構出發,精煉推導出我們自己的應用架構,並且將這個應用架構實現為Maven Archetype,最後使用我們Archetype創建一個簡單的CMS項目作為本文的落地案例。
需要明確的是,本文只是給讀者介紹了DDD應用架構,還有許多概念沒有涉及,例如實體、值對象、聚合、領域事件等,如果讀者對完整落地DDD感興趣,可以到本文最後瞭解更多。
二、應用架構演化
我們很多項目是基於三層架構的,其結構如圖:
圖片
我們說三層架構,為什麼還畫了一層 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 第三步、維護領域對象生命周期

在上一步中,loadDomain、saveDomain 這兩個方法還沒有得到討論,這兩個方法跟領域對象的生命周期息息相關。
關於領域對象的生命周期的詳細知識,讀者可以自行學習瞭解。
不管是 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執行業務處理,充血模型則是由模型進行業務處理。
“輸出”環節,業務邏輯執行完成之後將結果輸出到外部。
不管我們採用的什麼架構,其描述的應用的核心都是這個過程,不必生搬硬套非得用什麼應用架構。
正如《金剛經》所言:一切有為法,如夢幻泡影,如露亦如電,應作如是觀;凡所有相,皆是虛妄;若見諸相非相,即見如來。
三、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,填入項目信息並創建項目:
圖片
項目創建完成後:
圖片
四、代碼案例
本文提供了配套的代碼案例,該案例使用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 運行截圖

圖片
五、總結以及進一步學習
本文通過對貧血三層架構進行精煉,推導出適合我們落地的應用架構,並且將之實現為Maven Archetype以應用到實際開發,然而應用架構只是落地DDD的一個知識點,要完整落地DDD還必須體系化地掌握限界上下文、上下文映射、充血模型、實體、值對象、領域服務、Factory、Repository等知識點。

-end-

作者|覃玉傑

本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Hands-teach-you-how-to-land-DDD.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # Unity UGUI的RectMask2D(2D遮罩)組件的介紹及使用 ## 1. 什麼是RectMask2D組件? RectMask2D是Unity UGUI中的一個組件,用於實現2D遮罩效果。它可以限制子對象在指定的矩形區域內顯示,超出區域的部分將被遮罩隱藏。 ## 2. RectMask2 ...
  • # Unity UGUI的LayoutElement(佈局元素)組件的介紹及使用 ## 1. 什麼是LayoutElement組件? LayoutElement是Unity UGUI中的一個佈局元素組件,用於控制UI元素在佈局中的大小和位置。它可以用於自動調整UI元素的大小,以適應不同的屏幕解析度和 ...
  • # shell腳本-入侵檢測與告警 ## 原理 利用inotifywait命令對一些重要的目錄作一個實施監控,例如:當/root 、/usr/bin 等目錄發生改變的,利用inotifywait看可以對其作一個監控作用。 ## inotifywait ### 介紹 inotifywait 是一個 L ...
  • 首先來看一下需要操作的函數,以及配置的步驟: 圖1 圖2 Code: usart.c #include "usart.h"void ustart_Init(void ){ GPIO_InitTypeDef GPIO_Init_Ustar ; // 定義輸出埠TX的結構體對象 USART_InitT ...
  • ## 一、mysql安裝 在配置Hive之前一般都需要安裝和配置MySQL,因為Hive為了能操作HDFS上的數據集,那麼他需要知道數據的切分格式,如行列分隔符,存儲類型,是否壓縮,數據的存儲地址等信息。 為了方便以後操作所以他需要將這些信息通過一張表存儲起來,然後將這張表(元數據)存儲到mysql ...
  • 【JavaScript寫法】數組去重 在進行項目開發的時候,有時候需要把一些前端的數組進行去重處理,得到一個去重後的數據,然後再進行相關的操作,這也是在前端面試中經常出現的問題 ...
  • - Vue 初始化 - 模板渲染 - 組件渲染 為了便於理解,本文將從以下兩個方面進行探索: - 從 Vue 初始化,到首次渲染生成 DOM 的流程。 - 從 Vue 數據修改,到頁面更新 DOM 的流程。 # Vue 初始化 先從最簡單的一段 Vue 代碼開始: """ {{ message }} ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...