如何從0到1實踐DDD

5 評論 11059 瀏覽 75 收藏 24 分鐘

編輯導語:DDD(Domain-driven design,領域驅動設計)是一種架構設計方法論,通過邊界劃分,將復雜業務領域簡單化,幫助我們設計出清晰的領域和應用邊界,保證業務模型與代碼模型的一致性。本文作者結合實際經驗,介紹了如何從0到1實踐DDD,一起來看看吧。

隨著業務的不斷發展,我們發現自己的系統開始變得有點臃腫,為了減少復雜性,我們嘗試借助DDD來改善我們的系統。本文記錄了自己對DDD的理解和實踐過程,歡迎大家一起探討。見識所限,難免有理解不到位,希望路過的大佬不吝賜教。

一、為什么需要DDD

  • 當朋友和你聊工作時,你能否一語中的,說清你在開發中的業務內容及其價值?
  • 當產品和你聊需求時,你是否遇到過反復溝通之后才發現講的不是同個東西的情況?
  • 當你在做需求評估時,你是否經常發現一個小的需求改動,總是牽一發動全身?
  • 當你在快樂寫代碼時,你是否經常覺得有些類可有可無,有些接口望文不知義?

如果你有以上的一些疑問,那你可以試試領域驅動設計:

DDD(Domain-driven design,領域驅動設計)是一種架構設計方法論,通過邊界劃分,將復雜業務領域簡單化,幫助我們設計出清晰的領域和應用邊界,保證業務模型與代碼模型的一致性。

在細看這個定義之前,我們可以思考一下,為什么我們的業務系統會慢慢變得復雜?

常見的情況是,業務在發展過程中為了探尋發力點,需要不斷地試錯迭代,調整方向,而系統在設計之初,難以預期到后面的瞬息萬變,為了應付業務,修修改改,久之,系統也變得復雜起來。

可以怎么辦呢?及時重構唄——不改變軟件系統外部行為的前提下,改善它的內部結構。

然而重構是從技術層面上抽煉出來的模型,往往不具有實際的業務含義,其他同學可能難以自然地將業務問題映射到對應的設計模型。另外,如果不能如實映射業務模型,隨著業務方向調整,代碼可能又開始腐敗……有點像芝諾悖論中,阿基里斯永遠追不上小烏龜。

如何從0到1實踐DDD

那DDD怎么搞?

DDD是這么想的:”將業務架構映射到系統架構上,在響應業務變化調整業務架構時,也隨之變化系統架構”。可能大家平時有這樣的想法,但是比較模糊,未形成體系,而DDD就提供了一套完整的方法論。從業務角度去審視我們的系統,從而實現高內聚低耦合的代碼。

整體而言,領域驅動設計包括戰略建模和戰術建模: 戰略設計側重于高層次、宏觀上去劃分和集成限界上下文,而戰術設計則關注更具體使用建模工具來細化上下文。

二、 如何實現DDD之戰略建模

1. 基本概念

1)領域、子域

在討論問題之前,我們需要先定義好問題。

領域即問題域,通常是根據一個組織所處的行業進行識別,它基于業務的愿景,定義了系統要解決的現實問題的目標和范圍。領域越大,業務的范圍也越大,大的領域可以拆分成小的問題域,稱之為子域。根據子域重要性和功能屬性劃,可以將其分為三類。

核心域、支撐域和通用域:

  • 核心域:決定產品核心競爭力的子域
  • 支撐域:實現核心域目標所需的,但重要程度不如核心域的子域,一般具備強烈的個性化需求
  • 通用域:具有通用功能,可被多個子域使用的的是通用域。該子域所解決的問題一般是業界常見問題,有成熟的解決方案,可直接購買或簡單修改來使用

這個幾個概念其實很容易理解,不過在劃分的時候,注意要從業務的視角,而不是技術功能模塊來劃分。

2)限界上下文

我們語言博大精深,同樣的話在不同語境下就可演變出不同含義,這在溝通時總是帶來不必要的麻煩。為了準確地溝通,我們需要統一語言的邊界,在相同的語言邊界內溝通,才不容易出差錯。

一則阿凡提當理發師懲罰一個狡猾牧師的趣事:理發時,阿凡提刮臉時問牧師:“牧師,是否要眉毛?”牧師答:“這還用問,眉毛豈能不要?”.“好,你要我就給你!”,說著就把牧師的眉毛刮下來遞到他手里,牧師氣得說不出話來,誰叫自己說要呢。阿凡提又問:“牧師,胡子要嗎?”.“不要,不要!”牧師連忙說?!昂茫悴灰筒灰?。” 嗖嗖幾刀就把牧師的胡子刮下來。

在一個系統中,一個名詞在不同語境可能有不同的含義,我們對它關注的屬性和行為也有所不同。例如,在電商系統中,對于產品Product, 在采購上下文,需要關注產品的進價、最小起訂量與供貨周期;在市場上下文中,則關心產品的品質、售價,以及用于促銷的精美圖片和銷售類型;在倉儲上下文中,倉庫工作人員更關心產品放在倉庫的哪個位置,產品的重量與體積,是否易碎品以及訂購產品的數量。

限界上下文在《實現領域驅動設計》中,用了很大篇幅去講,它有幾個重要的意義:

  1. 限界上下文是領域概念的語言邊界與業務邊界:在這個邊界內,領域概念的內涵是清晰、無歧義的
  2. 限界上下文是團隊的工作邊界:組織邊界與限界上下文對齊
  3. 限界上下文是技術方案的實施邊界:在這個邊界內,技術方案是獨立自治的,業務邏輯不會落入不同技術邊界的間隙

經過戰略建模之后,我們可以得到以下的一個模型:

如何從0到1實踐DDD

2. 業務實踐

為了更好地理解,我們對手上的一個項目:“IoT設備增值產品管理系統”進行實踐。該項目中,我們提供給商戶在IoT設備上管理增值運營產品的能力。這里的IoT設備主要是微信支付刷臉設備等。商戶可以在系統中創建我們業務中的增值運營產品,如電子海報、互動海報等,創建完之后,相關的增值產品會被投放到IoT設備上,進行展示、運作:

如何從0到1實踐DDD如何從0到1實踐DDD

一開始我們從業務的用例出發,認為我們的系統主要是商戶在我們頁面網站使用,以及IoT設備通過接口連接我們后臺服務,認為這兩個分屬不同的子域,然后梳理了一些支撐的功能:

如何從0到1實踐DDD

畫完草圖之后,感覺不是很確定,于是便去咨詢部門的DDD專家王老師(十分感謝王立老師的指導),得到了一些寶貴的建議:我們應該避免直接從表現層去看業務,表現層就像是冰山露在水面上的棱角,這些棱角看起來毫不相干,但是實際上底層是連成一塊的,這些才是我們需要關注的。

就像這個項目,表面上商戶和設備是分開的,實際上它們在操作都是我們的增值運營產品,應該看成我們的系統提供統一對外的服務,然后商戶和設備來使用我們的服務。UGC內容存儲業務用例其實沒有涉及到的,屬于實現時候的東西。一番建議讓我們理清了思路,于是重新梳理,得到以下的戰略建模圖:

如何從0到1實踐DDD

整體而言,我們將整體系統梳理為8個子域:

  1. 增值運營服務子域:核心域,是我們業務主要競爭力。從業務上來講,我們的核心是通過提供業務中IoT設備上的增值運營服務
  2. 增值運營產品子域:支撐域,這里主要是我們提供增值運營產品,如電子海報、互動海報等
  3. 生效場景子域:支撐域,業務中增值運營產品有不同生效場景,這里統一進行管理
  4. 準入子域:支撐域,現主要是業務中對使用者的一些限制規則
  5. 權限管理子域:支撐域,基于角色來管理使用者的權限
  6. 商戶信息子域:支撐域,提供商戶的信息
  7. IoT設備信息子域:支撐域,提供IoT設備的信息
  8. 風險識別子域:通用域,識別業務中一些安全風險,如不合規的UGC素材等。這部分是業界常見問題,可以使用通用方案來解決,實際上我們也是接入TEG的能力來實現

其中我們系統中的商戶信息依賴了微信支付商戶賬號信息和IoT設備鋪設服務信息,這里使用防腐層進行隔離,將外部的商戶信息“翻譯”為我們業務中的商戶信息。三、如何實現DDD之戰術建模梳理清楚上下文之間的關系后,我們基本了解業務的概貌,接下來需要細化上下文,進一步完善我們的模型。這里也需要用到DDD的一些基本概念。

3. 基本概念

1)實體、值對象

實體和值對象是組成領域模型的基礎單元。當一個對象由其標識(而不是屬性)區分時,這種對象稱為實體。如在校園教務系統中,每個賬戶是對應著一個學生,根據學號來唯一標識,可以認為是一個實體。傳統的數據建模大多是根據數據庫范式設計的,每一個數據庫表對應一個實體,每一個實體的屬性值用單獨的一列來存儲,一個實體主表會對應 N 個實體從表。

與其不同,DDD 是先構建領域模型,再將業務對象映射為持久化對象。這可能導致DDD建立出來的實體,映射到具體數據庫表時,可能是1對多,多對1的關系。

如一個賬戶實體,有它的基本信息和權限角色信息,可能就對應了2個持久化對象。另一方面,有時候為了某些查詢場景的方便,會把教師賬戶、學生賬戶等對應成一個持久化對象,就成了多對1。

通過對象屬性值來識別的對象,則可以認為是一個值對象。如地址信息{“省”: “廣東省”,”市”:”深圳市”},我們是通過它的屬性來區分出不同的地址。值對象實際上是想把一些不變的屬性組合起來,減少系統的復雜性。在設計值對象的時候,需要滿足以下的特性:

  1. 值對象相等性:可以通過對其屬性的比較,來區分不同的值對象
  2. 不變性:需要保證值對象創建后就不能被修改,即不允許外部再修改其屬性
  3. 可替換性:值對象是一個整體,當其描述的對象有變化時,需要用一個新的值對象來替換對于值對象,由于其具有不變性,且是通過屬性來判斷相等的,在設計對應的數據庫持久化對象時,可以將其以JSON形式存儲在數據庫表的某一字段中

如何從0到1實踐DDD

2)聚合、聚合根

在 DDD 中,實體和值對象是基礎的領域對象。實體一般對應業務對象,它具有業務屬性和業務行為;而值對象主要是屬性集合,對實體的狀態和特征進行描述。但是我們的一個業務流程中,一般會同時涉及多個實體、值對象的操作,這里業務邏輯緊密的實體和值對象便組合成一個聚合。

從數據層面來看,同個聚合內的數據需要保持強一致性。

每一個聚合有一個聚合根實體,設置聚合根的主要目的是為了避免由于復雜數據模型缺少統一的業務規則控制,而導致聚合、實體之間數據不一致性的問題。聚合根可以看成是聚合的管理者,或是說handle。對內其協調實體和值對象完成業務邏輯。對外則提供通過聚合ID供其他聚合關聯引用,屏蔽外部對內部實體的直接訪問和修改。

建議的聚合設計原則:

  1. 在一致性邊界之內確保不變性:聚合用來封裝真正的不變性,而不是簡單地將對象組合在一起。聚合內有一套不變的業務規則,各實體和值對象按照統一的業務規則運行,實現對象數據的一致性。
  2. 設計小聚合:如果聚合聚合包含過多的實體,會提高管理實體的復雜性,高頻操作下容易并發沖突,降低了系統的性能。
  3. 在邊界之外使用最終一致性:不同的聚合之間不要求強一致性,保證最終一致性。一次事務操作中,只修改一個聚合實例,如果需要修改多個實例,可以考慮通過異步的方式保證最終一致性。

3)領域服務

領域服務的定義:領域中的服務表示一個無狀態的操作,它用于實現特定于某個領域的任務。當某個操作不適合放在聚合(實體)或值對像上時,最好的方式便是使用領域服務。

舉個例子,在一個路線導航的項目中,“路線”可能是其中的一個實體,如果業務中有“推薦路線上相關的美食”這樣一個功能,那我們會想,這個功能應該歸給哪個領域對象,給“路線”實體嗎?有點不合適,應該路線本身關注的是起終點,時間人物等。

此時可以將其這個功能歸為領域服務,它是一個路線狀態無關的服務,輸入路線各個節點,來得到沿路的各種美食。當然,要注意不要過度地使用領域服務,因為這很可能導致你把實體的行為都放在里面了,實體本身都變成了一些只有getter和setter的“貧血模型”。

4)領域事件

領域事件是領域模型中非常重要的一部分,用來表示領域中發生的事件。一個領域事件將導致進一步的業務操作,在實現業務解耦的同時,還有助于形成完整的業務閉環。

領域事件含義很廣泛,可以是業務流程的一個步驟,也可以是一個事件發生后觸發的后續動作,繳費完成之后,觸發短信通知;上面在設計聚合的時候,我們提到一個原則:在邊界之外使用最終一致性,一次事務最多只能更改一個聚合的狀態。如果一次業務操作涉及多個聚合狀態的更改,應通過領域事件,達到最終一致性。

實際上是通過事件驅動的這種異步方式,對系統進行解耦。當然,如果你覺得某兩個步驟,業務流程上不允許是不一致的,那就得重新考慮將其歸在同個聚合中了。

4. 業務實踐

我們以增值運營服務上下文為例,根據上面的理解,結合業務實際,得到以下模型:

如何從0到1實踐DDD

其中增值產品是其中的一個聚合根,通過該聚合根進行各種領域操作。海報縮略圖是其中的一個領域服務,通過輸入產品素材中的海報url,來得到一個海報的縮略圖。

四、工程實踐

傳統的三層架構和DDD的分層結構:

如何從0到1實踐DDD

在《領域驅動設計——軟件核心復雜性的應對之道》一書中,Eric提出了這樣的一種分層結構,將整個系統劃分為四層:用戶接口層、應用層、領域層和基礎設施層。

用戶接口層:用戶接口層負責向用戶顯示信息和解釋用戶指令。

應用層:應用層相對來說是較“薄”的一層,主要是部署了應用服務。應用服務的實現中,它負責編排和轉發下一層的領域層的接口,將要實現的功能委托給一個或多個領域對象來實現,本身只負責處理業務用例的執行順序以及結果的拼裝。

領域層:領域層是比較“厚”的一層,它包含聚合根、實體、值對象、領域服務等領域模型中的領域對象,實現了核心的業務邏輯。領域層和應用層的職責看起來有點模糊。

個人覺得,可以理解是應用層描述了一個具體操作從開始到結束的每一個環節,而領域層則是對其的細化,用來處理具體的某一個環節。

比如,比如線上購物中,購物車結算這一場景可看成是一個應用行為。而這個行為又主要包括金額計算、支付、生成訂單,這些子環節就可以理解為一個領域層的服務了。

基礎設施層:可以看到上面三層都有箭頭指向基礎設施層,它的作用就是為其它各層提供通用的技術和基礎服務,如數據持久化、消息中間件等DDD 分層架構中的要素與傳統三層架構(用戶界面層、業務邏輯層、數據訪問層)還是挺相似的,一個主要的變化是將業務邏輯層的服務拆分到了應用層和領域層。應用層響應業務用例的變化,領域層關注不變的領域模型。

如何從0到1實踐DDD

圖片來自極客時間

《DDD實戰課》在實際的代碼工程便是按照這樣的目錄來劃分,最近部門在推的整潔Git,也是這樣劃分目錄:

如何從0到1實踐DDD

接下來,便是將領域對象映射到實際的類,實現對應的屬性和行為。當然,具體實現中有很多范式可參考和討論,我們也在摸索中,待后續慢慢補充……

五、總結

DDD首先不是關于技術的,而是關于討論、聆聽、理解、發現業務價值的。——Vaughn Vernon《實現領域驅動設計》

如Vernon所說的,DDD首先是關注業務的價值的。一開始我們對業務的邊界、目標可能有個大概了解,但是見解還是不盡相同。

通過一起對業務的討論與思考,我們了解了業務的概貌及核心,明確價值所在。關注到了核心,自然可以幫助我們實現與業務契合的系統。通過這次學習與實踐,我們進一步接觸了DDD。

當然,這也還只是開始,更多的關聯知識還隱藏在冰山之下。同時我們也明白,DDD也只是一種方法論上的參考,不是“銀彈”,需要不斷地去實踐與思考,才能體會出它的價值。

參考:

  • Eric Evans.領域驅動設計.趙俐 盛海艷 劉霞等譯.人民郵電出版社,2016.
  • 美團技術團隊.領域驅動設計在互聯網業務開發中的實踐:https://tech.meituan.com/2017/12/22/ddd-in-practice.html?spm=a2c4e.10696291.0.0.428119a4uu9Gpl
  • 極客時間.DDD實戰課:https://time.geekbang.org/column/article/152677

 

作者:bryanzhao,微信支付后臺開發工程師

本文由 @騰訊大講堂 原創發布于人人都是產品經理,未經許可,禁止轉載。

題圖來自 Pixabay,基于CC0協議。

更多精彩內容,請關注人人都是產品經理微信公眾號或下載App
評論
評論請登錄
  1. 產品經理在DDD中應該輸出哪些內容?領域劃分?由領域抽象出來的實體?

    來自福建 回復
  2. 個人覺得DDD不應該替代代碼層的MVC結構,主要還是用來做產品和微服務架構的指導,用來劃分清楚系統模塊邊界

    來自天津 回復
  3. 業務架構都已經理解,但是關于技術方面的架構設計還是不太理解

    來自湖南 回復
  4. 同上,不適合小白

    來自湖南 回復
  5. DDD,哈哈哈,我感覺因該叫3D,乍一眼還真的不知道要講什么,看完之后,果然不是干這個的,真的不怎么清楚。

    來自河南 回復