天天看點

産品代碼都給你看了,可别再說不會DDD系列(四):代碼工程結構

作者:碼如雲

這是一個講解DDD落地的文章系列,它以一個真實的并已成功上線的軟體項目為例,系統性地講解DDD在落地實施過程中的典型實踐,以及在面臨實際業務場景時的各種取舍。

本系列包含以下文章:

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

案例項目介紹

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

碼如雲是一個針對二維碼場景應用的SaaS軟體平台,采用“一物一碼”的業務模式,可以為每一件“物品”生成一個二維碼,并以該二維碼為入口展開對“物品”的相關操作,典型的應用場景包括固定資産管理、裝置巡檢以及物品标簽等。

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

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

代碼工程結構

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

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

在上一篇戰略設計中我們提到,碼如雲是一個單體項目,其通過Java分包的方式劃分出了3個限界上下文,即3個子產品。對于正在搞微服務的讀者來說,可不要被“單體”二字吓跑了,本文所講解的絕大多數内容既适合于單體,也适合于微服務。

産品代碼都給你看了,可别再說不會DDD系列(四):代碼工程結構

以上是碼如雲工程的目錄結構,在根分包src/main/java/com/mryqr下,分出了core、integration和management3個子產品包,分别對應“核心上下文”、“內建上下文”和“背景管理上下文”,對于微服務系統來說,這3個分包則不存在,因為每個分包都有自己單獨的微服務項目,也即DDD的限界上下文和微服務存在一一對應的關系。與這3個子產品包同級的還有一個common包,該包并不是一個業務子產品,而是所有子產品所共享的一些基礎設施,比如Spring的配置、郵件發送機制等。在src目錄下,還包含test、apiTest和gatling三個目錄,分别對應單元測試,API測試和性能測試代碼。此外,deploy目錄用于存放與部署相關的檔案,doc目錄用于存放項目文檔,gradle目錄則用于存放各種Gradle配置檔案。

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

在以上提及的各種子產品包中,程式員們最為關注的估計是core包之下應該如何進一步分包了,因為core是整個項目的核心業務子產品。

産品代碼都給你看了,可别再說不會DDD系列(四):代碼工程結構

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

産品代碼都給你看了,可别再說不會DDD系列(四):代碼工程結構

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

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

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

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

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

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

産品代碼都給你看了,可别再說不會DDD系列(四):代碼工程結構

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

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

産品代碼都給你看了,可别再說不會DDD系列(四):代碼工程結構

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

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

産品代碼都給你看了,可别再說不會DDD系列(四):代碼工程結構

在command包中,應用服務AppCommandService用于接收外界的業務請求(指令)。AppCommandService接收的輸入參數為Command對象(以“Command”為字尾),Command對象通過其名稱表達業務意圖,比如CopyAppCommand用于拷貝應用(這裡的“應用”表示業務上的應用聚合根),CreateAppCommand用于建立應用。

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

産品代碼都給你看了,可别再說不會DDD系列(四):代碼工程結構

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

産品代碼都給你看了,可别再說不會DDD系列(四):代碼工程結構

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

産品代碼都給你看了,可别再說不會DDD系列(四):代碼工程結構

自動化測試

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

總結

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