產品程式碼都給你看了,可別再說不會DDD(四):程式碼工程結構

2023-08-26 21:02:17

這是一個講解DDD落地的文章系列,作者是《實現領域驅動設計》的譯者滕雲。本文章系列以一個真實的並已成功上線的軟體專案——碼如雲https://www.mryqr.com)為例,系統性地講解DDD在落地實施過程中的各種典型實踐,以及在面臨實際業務場景時的諸多取捨。

本系列包含以下文章:

  1. DDD入門
  2. DDD概念大白話
  3. 戰略設計
  4. 程式碼工程結構(本文)
  5. 請求處理流程
  6. 聚合根與資源庫
  7. 實體與值物件
  8. 應用服務與領域服務
  9. 領域事件
  10. CQRS

案例專案介紹

既然DDD是「領域」驅動,那麼我們便不能拋開業務而只講技術,為此讓我們先從業務上了解一下貫穿本文章系列的案例專案 —— 碼如雲(不是馬雲,也不是碼雲)。如你已經在本系列的其他文章中瞭解過該案例,可跳過。

碼如雲是一個基於二維條碼的一物一碼管理平臺,可以為每一件「物品」生成一個二維條碼,並以該二維條碼為入口展開對「物品」的相關操作,典型的應用場景包括固定資產管理、裝置巡檢以及物品標籤等。

在使用碼如雲時,首先需要建立一個應用(App),一個應用包含了多個頁面(Page),也可稱為表單,一個頁面又可以包含多個控制元件(Control),比如單選框控制元件。應用建立好後,可在應用下建立多個範例(QR)用於表示被管理的物件(比如機器裝置)。每個範例均對應一個二維條碼,手機掃碼便可對範例進行相應操作,比如檢視範例相關資訊或者填寫頁面表單等,對錶單的一次填寫稱為提交(Submission);更多概念請參考碼如雲術語

在技術上,碼如雲是一個無程式碼平臺,包含了表單引擎、審批流程和資料包表等多個功能模組。碼如雲全程採用DDD完成開發,其後端技術棧主要有Java、Spring Boot和MongoDB等。

碼如雲的原始碼是開源的,可以通過以下方式存取:

碼如雲原始碼:https://github.com/mryqr-com/mry-backend

程式碼工程結構

碼如雲,我們經常會受邀去給其他公司或組織分享DDD的落地實踐經驗,分享期間聽眾一般會問很多問題,被問得最多的反倒不是限界上下文如何劃分,聚合如何設計等DDD重點議題,而是DDD工程結構該怎麼搭,包該怎麼分這些實實在在的問題。

事實上,DDD並未對工程結構做出要求,在碼如雲,我們結合行業通用實踐以及自身對DDD的認識搭建出了一套適合於自身的工程結構,我們認為對於多數專案也是適用的,在本文中,我們將對此做詳細講解。

在上一篇戰略設計中我們提到,碼如雲是一個單體專案,其通過Java分包的方式劃分出了3個限界上下文,即3個模組。對於正在搞微服務的讀者來說,可不要被「單體」二字嚇跑了,本文所講解的絕大多數內容既適合於單體,也適合於微服務。

以上是碼如雲工程的目錄結構,在根分包src/main/java/com/mryqr下,分出了coreintegrationmanagement3個模組包,分別對應「核心上下文」、「整合上下文」和「後臺管理上下文」,對於微服務系統來說,這3個分包則不存在,因為每個分包都有自己單獨的微服務專案,也即DDD的限界上下文和微服務存在一一對應的關係。與這3個模組包同級的還有一個common包,該包並不是一個業務模組,而是所有模組所共用的一些基礎設施,比如Spring的設定、郵件傳送機制等。在src目錄下,還包含testapiTestgatling三個目錄,分別對應單元測試,API測試和效能測試程式碼。此外,deploy目錄用於存放與部署相關的檔案,doc目錄用於存放專案檔案,gradle目錄則用於存放各種Gradle組態檔。

分包原則:先業務,後技術

在以上提及的各種模組包中,程式設計師們最為關注的估計是core包之下應該如何進一步分包了,因為core是整個專案的核心業務模組。

在做分包時,一個最常見的反模式是將技術分包作為上層分包,然後在各技術分包下再劃分業務包。DDD社群更加推崇的分包方式是「先業務,後技術」,即上層包先按照業務進行劃分,然後在各個業務包內部可以再按照技術分包。

在碼如雲的core模組包中,首先是基於業務的分包,包含app、 assignment等幾十個包,其中的app對應於應用聚合根,而assignment對應於任務聚合根,也即每一個業務分包對應一個聚合根。在每個業務分包下再做技術分包,其中包含以下子分包:

  • command:用於存放應用服務以及命令物件等,更多相關內容請參考應用服務與領域服務
  • domain:用於存放所有領域模型,更多相關內容請參考聚合根與資源庫
  • eventhandler:用於存放領域事件處理器,更多相關內容請參考領域事件;
  • infrastructure:用於存放技術基礎設施,比如對資料庫的存取實現等;
  • query:用於存放查詢邏輯,更多相關內容請參考CQRS

在這些分包下,可以根據實際情況進一步分包。

這種「先業務,後技術」的分包方式有以下好處:

  • 業務直觀:所有的業務模組被放在一起,並且處於一個分包級別中,讓人一眼即可全景式地瞭解一個軟體專案中的所有業務。事實上,Robert C. Martin(Bob大叔)提出了一個概念叫尖叫架構(Screaming Architecture)講的就是這個意思。尖叫即「哇的一聲」的意思,比如當你看到一棟房子時,你會說「哇,好一棟漂亮的房子!」,也即你一眼就能識別出這是一套房子。
  • 便於導航:當你要查詢一個功能時,你首先想到的一定是該功能屬於哪個業務板塊,而不是屬於哪個Controller,因此你可以先找到業務分包,然後順藤摸瓜找到相應的功能程式碼。
  • 便於遷移:每一個業務包都包含了從業務到技術的所有程式碼,因此在遷移時只需整體挪動業務包即可,比如,如果碼如雲以後要遷移到微服務架構,那麼只需將需要遷出的業務包整體拷貝到新的工程中即可。

在以上子分包中,domain分包應是最大的一個分包,因為其中包含了所有的領域模型以及業務邏輯。在碼如雲專案的app業務包下,各個子分包所包含的程式碼量統計如下:

可以看到,domain包中所包含的程式碼量遠遠超過其他所有分包的總和。當然,我們並不是說所有DDD專案都需要滿足這一點,而是強調在DDD中領域模型應該是程式碼的主體。

接下來,讓我們來看看各個子分包中都包含哪些內容,首先來看domain分包:

domain分包中,最重要的當屬App聚合根了,除此之外還包含領域服務AppDomainService,工廠AppFactory和資源庫AppRepository。這裡的AppRepository是一個介面,其實現在infrastructure分包中。基於內聚原則,有些密切聯絡的類被放置在了下一級子分包中,比如attributepage分包等。值得一提的是,用於存放領域事件的event包也被放置在了domain下,因為領域事件也是領域模型的一部分,不過領域事件的處理器類則放在了與domain同級的eventhandler包中,我們將在 領域事件中對此做詳細講解。

command包用於放置應用服務以及請求資料類,這裡的「command」即CQRS中的「C」,表示外界向軟體系統所發起的一次命令。

command包中,應用服務AppCommandService用於接收外界的業務請求(命令)。AppCommandService接收的輸入引數為Command物件(以「Command」為字尾),Command物件通過其名稱表達業務意圖,比如CopyAppCommand用於拷貝應用(這裡的「應用」表示業務上的應用聚合根),CreateAppCommand用於新建應用

eventhandler用於存放領域事件的處理器類,這些類的地位相當於應用服務,它們並不是領域模型的一部分,只是與應用服務相似起編排協調作用。

infrastructure用於存放基礎設施類,主要包含資源庫的實現類:

query用於存放與資料查詢相關的類,這裡的"query"也即CQRS中的「Q」,我們將在本系列的CQRS中對此做詳細講解。

自動化測試

自動化測試包含單元測試、API測試和效能測試。在API測試中,資料庫和訊息佇列等基礎設施均通過本地Docker完成搭建,測試時先啟動整個Spring程序,然後模擬前端向各個API傳送真實業務請求,最後驗證返回結果,如果遇到有需存取第三方系統的情況,則通過Stub類進行代替。碼如雲採用的是「API測試為主,單元測試為輔」的測試策略,其API測試覆蓋率達到了90%,所有的業務用例和重要分支都有API測試覆蓋,單元測試主要用於測試領域模型,對於諸如應用服務、Controller以及事件處理器等結構性設施則不作單元測試要求,因為這些類並不包含太多邏輯,對這些類的測試可以消化在API測試中。

總結

本文主要講解了DDD程式碼工程的典型目錄結構,我們推薦通過「先業務,後技術」的方式進行分包,這樣使得專案所體現的業務更加的直觀。此外,在DDD專案中,領域模型應該是整個專案的主體,所有的領域物件和業務邏輯均應該包含在domain包下。在下文請求處理流程中,我們將對DDD專案中請求處理的全流程進行詳細講解。