這是一個講解DDD落地的文章系列,作者是《實現領域驅動設計》的譯者滕雲。本文章系列以一個真實的並已成功上線的軟體項目——碼如雲(https://www.mryqr.com)為例,系統性地講解DDD在落地實施過程中的各種典型實踐,以及在面臨實際業務場景時的諸多取捨。 本系列包含以下文章: DDD入門 ...
這是一個講解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。
在這些分包下,可以根據實際情況進一步分包。
這種“先業務,後技術”的分包方式有以下好處:
- 業務直觀:所有的業務模塊被放在一起,並且處於一個分包級別中,讓人一眼即可全景式地瞭解一個軟體項目中的所有業務。事實上,Robert C. Martin(Bob大叔)提出了一個概念叫尖叫架構(Screaming Architecture)講的就是這個意思。尖叫即“哇的一聲”的意思,比如當你看到一棟房子時,你會說“哇,好一棟漂亮的房子!”,也即你一眼就能識別出這是一套房子。
- 便於導航:當你要查找一個功能時,你首先想到的一定是該功能屬於哪個業務板塊,而不是屬於哪個Controller,因此你可以先找到業務分包,然後順藤摸瓜找到相應的功能代碼。
- 便於遷移:每一個業務包都包含了從業務到技術的所有代碼,因此在遷移時只需整體挪動業務包即可,比如,如果碼如雲以後要遷移到微服務架構,那麼只需將需要遷出的業務包整體拷貝到新的工程中即可。
在以上子分包中,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項目中請求處理的全流程進行詳細講解。