深入淺出 DDD 編程

作者 | 劉嘿嘿、離夏、立羽

導讀

最近幾年,微服務拆分大行其道,在業務越來越複雜的情況下,許多業務紛紛拋棄了傳統單體架構,擁抱微服務。但隨着微服務的拆分結束,大家又發現了新的問題,比如服務間邏輯複雜,運維複雜性變高,微服務架構變得越來越難以管理,最終演化成大泥球架構。

而本文主要介紹如何通過 DDD 對微服務進行拆分,首先介紹了什麼是 DDD,通過從分析 DDD 的優勢,到如何通過 DDD 進行業務拆分,並且在最後通過代碼樣例的方式,深入淺出的爲讀者介紹了 DDD 代碼的核心實現。幫助大家進一步的瞭解 DDD 應該如何落地。

01 什麼是 DDD

DDD(領域驅動設計),起源於 2004 年 Eric Evans 出版《領域驅動設計》,近些年由於微服務的興起,大家逐漸對單體服務進行拆分。

但是隨着微服務拆分,由於業務邏輯拆分不合理導致調用環路問題、重試風暴問題等等,都給系統造成了更多的風險,並且隨着業務更加複雜微服務職責劃分出現問題,則業務迭代效率變得越來越差,最終變成一個大泥球系統。

而 DDD 的優勢便是指導業務進行微服務拆分,下面我們以會員中心爲例來具體講解一下如何進行業務拆分以及相關的代碼實現。

02 使用 DDD 的優勢是什麼

2.1 語言統一,消除誤解

很多時候未必產品經理纔是最懂業務的那個人,例如某些 B 端服務很多時候是運營人員在向產品同學提需求,在經過產品經理的翻譯後,才轉化成一個需求文檔,這樣就會導致有時候產品經理並不能完全表達出實際的需求,這就會導致開發人員交付的軟件無法達到預期。從而導致返工,浪費人力。

而 DDD 需要設計一種通用的語言,拉齊各個需求方的理解,一旦產品同學和技術同學對業務具備了相同的理解,統一的語言,那在後續的需求迭代種就會變得非常順暢。

在改造初期我們耗費了非常大的精力向產品同學講清楚哪些抽象應該定義爲實體,實體與實體的關係是什麼,在不斷的溝通、磨合中,最好我們成功建立起了一些通用的語言,拉齊了產品經理、運營同學、開發人員的理解,最大幅度的消除了由於理解不一致導致的返工、重構等工作。

2.2 更專注於業務的戰略設計

戰略設計側重於業務梳理,結合業務流程劃分對應的核心域、通用域、支撐域。戰略設計的核心價值是圍繞產品規劃重點投入資源,確保重點子業務可以確保得到足夠的人力支持。

2.3 設計即代碼,代碼即設計

在過去的項目詳細設計中,我們的重心在數據怎麼存儲?數據流通是什麼樣的。這樣可能導致在設計文檔和代碼中就具備較大的 Gap,實現上就可能有問題。

而 DDD 倡導的是思考,而不是寫代碼。在代碼設計之前定義好領域語言,和領域專家溝通無礙,定義好領域規則,這樣在寫代碼的時候留下較少的思考。代碼只是把設計文檔翻譯成代碼,寫代碼更像是在照着設計文檔在做填空題,只需要將代碼填到指定的文件中即可。

03 如何使用 DDD

3.1 DDD 戰略設計

3.1.1 劃分核心域,通用域、支撐域

在實際的工作中,很多產品經理會陷入到各種繁雜的業務指標中,無法從繁雜的業務中抽身,定義好哪些是重要的模塊,或者無法表達出業務各個模塊中最重要的是什麼。這種情況就會導致每個,產品沒有這就會導致在人員分工、資源申請上出現一些問題。

做戰略設計,最核心的事情就是劃分清楚核心域,通用域、支撐域,我們把更多的精力投入到核心的問題中,而不被大量次要的問題淹沒。

本處僅僅描述我們對於戰略設計理解,不對戰略設計展開說明。

3.1.2 劃分邊界

微服務職責的劃分是執行環節的第一步,也是最重要的一步,尤其從大單體拆分爲多個微服務時,需要考慮以下幾點

  1. 要通過領域驅動劃分邊界,若暫時考慮不清楚邊界,那就先不要拆分

  2. 明確微服務分層,上游服務只能對下游服務產生依賴,防止微服務環路調用問題,同時下游服務需要考慮重試風暴問題

  3. 核心域的微服務需要具備故障降級,容災能力

  4. 要基於組織架構進行邊界的劃分,微服務的梳理其實也是團隊的梳理,過度的拆分可能導致更多的溝通成本

3.2 DDD 戰術設計

3.2.1 名詞解釋

聚合與聚合根:是一組相關對象的組合,可以作爲拆分微服務的最小單位,具有高內聚、低耦合的特點,聚合在 DDD 中是一個很重要的概念,核心領域往往都需要用聚合來表達;聚合根爲其根節點,聚合根有實體的特點,具有全局唯一標識,有獨立的生命週期。一個聚合只有一個聚合根,聚合根在聚合內對實體和值對象採用直接對象引用的方式進行組織和協調,聚合根與聚合根之間通過 ID 關聯的方式實現聚合之間的協同。

領域服務:一些重要的領域行爲或操作,可以歸類爲領域服務。它既不是實體,也不是值對象的範疇。

領域事件:領域事件是對領域內發生的活動進行的建模。

實體:多個屬性、行爲及操作的載體,實體有全局唯一性標識(ID),有獨立的生命週期。例如會員用戶中,每個會員都可以被認爲一個實體,都有 userid 唯一性標識。

值對象:通過對象屬性來識別的對象,沒有標識符概念,無生命週期,只描述業務屬性。如在一個會員系統中,會員權益信息集合即可看爲一個值對象,只用於對權益屬性的描述,只有數據初始化操作和有限的不涉及修改數據的行爲。

3.2.2 如何進行戰術設計

接下來我們以會員中心爲例爲大家詳細介紹

在戰略模型中我們已經劃分清楚邊界,梳理不同領域及相關關係。接下來我們需要從戰術層面上剖析領域模型內部之間的關係,對會員上下文進行建模(下文爲簡化版)。

在會員上下文中,我們以會員實體爲中心,通過會員(vipinfo)這個聚合根來控制會員權限,一個會員包括用戶 ID(uid)、會員權益 (Privilege)、所屬機構(tp)以及會員碼(vip_code),而會員碼針對訂單維度分別對應不同的權益內容(privilege)。

這些值對象不具有業務行爲特徵,只關心本身屬性值。會員實體具有業務行爲及業務邏輯,例如會員入駐、會員變更、會員綁碼等,外部訪問會員權益值對象等都需要通過會員實體來進行。

在會員域中,我們同時支持會員碼及會員維度的領域服務,包括購買、獲取會員信息、綁碼等服務。

3.3 DDD 代碼實現

3.3.1 項目介紹

3.3.2 項目結構

項目結構如圖所示分爲四層、對應到到代碼目錄上(附錄 1),代碼一級目錄有 interface(接口層)、application(應用層)、domain(領域層)、infrastructure(基礎層)四個目錄。

接口層:

接口層處理接口定義、批處理相關邏輯。目錄如下:

|-- interface
|   |-- command // 批處理接口層
|   |   |-- controller 
|   |   |   `-- vip
|   |   |       |-- add.go 
|   |   |       `-- update.go
|   |   |-- router.go // 代碼入口定義
|   |   `-- script.go
|   `-- http // api接口層
|       |-- controller // 接口入參校驗、定義,調用下層代碼
|       |   |-- lawyer
|       |   |   |-- add.go
|       |   |   `-- update.go
|       |   `-- vipcode
|       |       |-- add.go
|       |       `-- update.go
|       |-- router.go // api路由

interface 目錄下有 command、http 兩個目錄,其中,

command:包含批處理入口,批處理路由,編排批處理相關領域層服務、事件、實體和基礎層相關函數。批處理代碼無需應用層直接依賴領域層、基礎層,降低代碼冗餘度。

http:包含接口路由、定義,接口入參校驗、定義。

應用層:

主要負責組織、編排領域層服務、事件、實體和基礎層相關函數。

application 下有 service、viewmodel。

|-- application
|   |-- service //應用層服務
|   |   |-- lawyer
|   |   |   |-- add.go
|   |   |   `-- update.go
|   |   `-- vip
|   |       |-- add.go
|   |       `-- update.go
|   `-- viewmodel // 視圖
|       |-- lawyer
|       |   |-- transform.go // 轉化函數
|       |   `-- vm.go //視圖數據結構
|       `-- vip
|           |-- transform.go
|           `-- vm.go

service: 對多個領域服務、基礎層 ral 調用、數據持久化服務進行封裝、編排,爲上層提供更粗粒度的服務,調用領域層服務,倉儲和事件,因爲鬆散分層結構,也可以調用基礎層服務。

viewmodel: 爲上層多變的數據結構要求,提供相應視圖定義和實體到視圖的轉化方法。

領域層:

領域層存放業務核心邏輯包括聚合根、實體、值對象、倉儲接口、領域服務、領域事件接口等。

領域層下分有五個目錄:

|-- domain
|   |-- aggregate // 聚合
|   |   |-- lawyer
|   |   |   |-- entity.go // 實體定義
|   |   |   `-- vo.go // 值對象
|   |   `-- vipcode
|   |       |-- entity.go
|   |       |-- vo.go
|   |-- event // 領域事件
|   |   `-- vipcode
|   |       `-- order.go
|   |-- repository // 倉儲接口
|   |   |-- lawyer.go
|   |   `-- vipcode.go
|   |-- adaptor // 防腐層
|   |   `-- sms.go
|   `-- service // 領域服務
|       |-- lawyer
|       |   `-- vipcode.go
|       `-- vipcode
|           `-- vipcode.go

aggregate:放置聚合根,實體、值對象數據結構定義,以及相關初始化代碼。

領域內數據流轉處理依賴,相關聚合根,下游服務發生改變——如數據表結構變換,只需將相關數據轉化爲業務定義聚合根,代碼更改只需在基礎層,不涉及上層。

下面是會員碼實體示例,裏面又包含有訂單值對象,會員碼機構值對象和會員碼權益值對象。

// EntityVipCode 會員碼實體(簡化版本)
type EntityVipCode struct {
  ValidityStart *time.Time       // 綁碼開始時間
  ValidityEnd   *time.Time       // 綁定會員碼結束時間
  OrderInfo     *VOOrderInfo     // 訂單信息值對象
  BuyerInfo     *VOCodeBuyerInfo // 買會員碼機構信息
  PrivilegeInfo *VOPrivilege     // 會員碼包含的權益
}

event:放置基礎層事件抽象的接口——爲了實現依賴倒置。

repository:放置基礎層數據持久化服務抽象的接口。

service:存放一下領域服務代碼,嚮應用層服務提供方法調用,依賴倒置在 ddd 中使用頻繁 。

adaptor:存放防腐層數據結構定義、轉化函數。

防腐層在下游服務和上游服務之間,將下游服務翻譯爲上游服務語言,拋去無需關注的,防止上層服務摻雜過多無需關注雜質。

ddd 中廣泛應用了依賴倒置原則(即調用要依賴於抽象接口,不要依賴於具體實現),減少 ddd 各層之間的耦合性,提高系統的穩定性,減少並行開發風險,提高代碼的可讀性和可維護性,非常適合 ddd 這樣爲應對頻繁迭代的設計思想。

如下創建訂單體現依賴倒置思想,無需關注具體實現,假若使用訂單方發生了變更(如更換服務提供方、服務提供方更換實現邏輯或者服務實現邏輯更改了),我們在上層代碼只需要更改傳入的參數,無需關注其他變更。

// ReqCreateOrder 創建訂單
func ReqCreateOrder(ctx context.Context, vipRepo repository.IVipCodeRepo, vipcodeentity vipcode.EntityVipCode) (*order.PreorderRetData, error)
type IVipCodeRepo interface {
  CreateOrder(ctx context.Context, ev vipcode.EntityVipCode) (*liborder.PreorderRetData, error)
  UpdateVipCode(ctx context.Context, patch map[string]interface{}, conditions map[string]interface{}) (int64, error)
}
基礎設施層:

基礎層存放領域事件、數據持久化、ral 調用相關代碼。

其下有三個目錄:

|-- infrastructure
|   |-- event // 領域事件實現
|   |   |-- init.go
|   |   `-- vipcode
|   |       `-- consume_order.go
|   |-- persistence // 持久化存儲實現
|   |   |-- init.go
|   |   |-- lawyer
|   |   |   |-- po.go
|   |   |   |-- repo.go
|   |   |   `-- transform.go
|   |   `-- vipcode
|   |       |-- po.go
|   |       |-- repo.go
|   |       `-- transform.go
|   `-- rpc // rpc調用實現
|       |-- db
|       |-- init.go
|       |-- redis

event:領域事件具體實現,依賴 rpc 服務。

persistence:放置儲存持久化代碼,數據庫存儲對應 PO,和 PO 到實體的轉化方法,所有具體實現方法都出參都需要轉化成實體供給上層使用。

rpc:rpc 遠程調用其他微服務、消息中間件等服務代碼。

04 總結

用好 DDD 的關鍵,就是理解 DDD 和核心思想,其本質也是面向對象的設計方法,即是把業務模型轉換爲對象模型從而來控制業務持續變化而導致系統的複雜性,使得系統更加具有可擴展性、可維護性。

在相對比較小、邏輯簡單的微服務,在代碼實現層面,我們並沒有按照 DDD 進行開發,傳統的 MVC 足以應對,若強行使用 DDD 則會徒增大家的工作量。

DDD 的核心是通過戰略設計來匹配產品層面的業務規劃,在戰術設計層面通過對每個模塊進行抽象、建模來完成業務梳理劃分邊界,在代碼實現層面來完成設計文檔到代碼的映射,做到設計即代碼、代碼即設計。

而 DDD 只適用於大型的、複雜的業務場景。切勿爲了 DDD 而 DDD。

參考資料:

[1] 《領域驅動設計》

[2]《實現領域驅動設計》

[3]https://mp.weixin.qq.com/s/y57l-PhzibAjjL3EzPqSow

[4]https://mp.weixin.qq.com/s/_ggIPOvB-ptBanbqqKULxQ

[5]https://mp.weixin.qq.com/s/jU0awhez7QzN_nKrm4BNwg

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/oXf84xykPVqtjToWrldM9Q