這是一個講解DDD落地的文章系列,作者是《實現領域驅動設計》的譯者滕雲。本文章系列以一個真實的並已成功上線的軟體專案——碼如雲(https://www.mryqr.com)為例,系統性地講解DDD在落地實施過程中的各種典型實踐,以及在面臨實際業務場景時的諸多取捨。
本系列包含以下文章:
既然DDD是「領域」驅動,那麼我們便不能拋開業務而只講技術,為此讓我們先從業務上了解一下貫穿本文章系列的案例專案 —— 碼如雲(不是馬雲,也不是碼雲)。如你已經在本系列的其他文章中瞭解過該案例,可跳過。
碼如雲是一個基於二維條碼的一物一碼管理平臺,可以為每一件「物品」生成一個二維條碼,並以該二維條碼為入口展開對「物品」的相關操作,典型的應用場景包括固定資產管理、裝置巡檢以及物品標籤等。
在使用碼如雲時,首先需要建立一個應用(App),一個應用包含了多個頁面(Page),也可稱為表單,一個頁面又可以包含多個控制元件(Control),比如單選框控制元件。應用建立好後,可在應用下建立多個範例(QR)用於表示被管理的物件(比如機器裝置)。每個範例均對應一個二維條碼,手機掃碼便可對範例進行相應操作,比如檢視範例相關資訊或者填寫頁面表單等,對錶單的一次填寫稱為提交(Submission);更多概念請參考碼如雲術語。
在技術上,碼如雲是一個無程式碼平臺,包含了表單引擎、審批流程和資料包表等多個功能模組。碼如雲全程採用DDD完成開發,其後端技術棧主要有Java、Spring Boot和MongoDB等。
碼如雲的原始碼是開源的,可以通過以下方式存取:
在碼如雲,我們經常會受邀去給其他公司或組織分享DDD的落地實踐經驗,分享期間聽眾一般會問很多問題,被問得最多的反倒不是限界上下文如何劃分,聚合如何設計等DDD重點議題,而是DDD工程結構該怎麼搭,包該怎麼分這些實實在在的問題。
事實上,DDD並未對工程結構做出要求,在碼如雲,我們結合行業通用實踐以及自身對DDD的認識搭建出了一套適合於自身的工程結構,我們認為對於多數專案也是適用的,在本文中,我們將對此做詳細講解。
在上一篇戰略設計中我們提到,碼如雲是一個單體專案,其通過Java分包的方式劃分出了3個限界上下文,即3個模組。對於正在搞微服務的讀者來說,可不要被「單體」二字嚇跑了,本文所講解的絕大多數內容既適合於單體,也適合於微服務。
以上是碼如雲工程的目錄結構,在根分包src/main/java/com/mryqr
下,分出了core
、integration
和management
3個模組包,分別對應「核心上下文」、「整合上下文」和「後臺管理上下文」,對於微服務系統來說,這3個分包則不存在,因為每個分包都有自己單獨的微服務專案,也即DDD的限界上下文和微服務存在一一對應的關係。與這3個模組包同級的還有一個common
包,該包並不是一個業務模組,而是所有模組所共用的一些基礎設施,比如Spring的設定、郵件傳送機制等。在src
目錄下,還包含test
、apiTest
和gatling
三個目錄,分別對應單元測試,API測試和效能測試程式碼。此外,deploy
目錄用於存放與部署相關的檔案,doc
目錄用於存放專案檔案,gradle
目錄則用於存放各種Gradle組態檔。
在以上提及的各種模組包中,程式設計師們最為關注的估計是core
包之下應該如何進一步分包了,因為core
是整個專案的核心業務模組。
在做分包時,一個最常見的反模式是將技術分包作為上層分包,然後在各技術分包下再劃分業務包。DDD社群更加推崇的分包方式是「先業務,後技術」,即上層包先按照業務進行劃分,然後在各個業務包內部可以再按照技術分包。
在碼如雲的core
模組包中,首先是基於業務的分包,包含app
、 assignment
等幾十個包,其中的app
對應於應用聚合根,而assignment
對應於任務聚合根,也即每一個業務分包對應一個聚合根。在每個業務分包下再做技術分包,其中包含以下子分包:
command
:用於存放應用服務以及命令物件等,更多相關內容請參考應用服務與領域服務;domain
:用於存放所有領域模型,更多相關內容請參考聚合根與資源庫;eventhandler
:用於存放領域事件處理器,更多相關內容請參考領域事件;infrastructure
:用於存放技術基礎設施,比如對資料庫的存取實現等;query
:用於存放查詢邏輯,更多相關內容請參考CQRS。在這些分包下,可以根據實際情況進一步分包。
這種「先業務,後技術」的分包方式有以下好處:
在以上子分包中,domain
分包應是最大的一個分包,因為其中包含了所有的領域模型以及業務邏輯。在碼如雲專案的app
業務包下,各個子分包所包含的程式碼量統計如下:
可以看到,domain
包中所包含的程式碼量遠遠超過其他所有分包的總和。當然,我們並不是說所有DDD專案都需要滿足這一點,而是強調在DDD中領域模型應該是程式碼的主體。
接下來,讓我們來看看各個子分包中都包含哪些內容,首先來看domain
分包:
在domain
分包中,最重要的當屬App
聚合根了,除此之外還包含領域服務AppDomainService
,工廠AppFactory
和資源庫AppRepository
。這裡的AppRepository
是一個介面,其實現在infrastructure
分包中。基於內聚原則,有些密切聯絡的類被放置在了下一級子分包中,比如attribute
和page
分包等。值得一提的是,用於存放領域事件的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專案中請求處理的全流程進行詳細講解。