如何從0到1實踐DDD
編輯導語:DDD(Domain-driven design,領域驅動設計)是一種架構設計方法論,通過邊界劃分,將復雜業(yè)務領域簡單化,幫助我們設計出清晰的領域和應用邊界,保證業(yè)務模型與代碼模型的一致性。本文作者結合實際經(jīng)驗,介紹了如何從0到1實踐DDD,一起來看看吧。
隨著業(yè)務的不斷發(fā)展,我們發(fā)現(xiàn)自己的系統(tǒng)開始變得有點臃腫,為了減少復雜性,我們嘗試借助DDD來改善我們的系統(tǒng)。本文記錄了自己對DDD的理解和實踐過程,歡迎大家一起探討。見識所限,難免有理解不到位,希望路過的大佬不吝賜教。
一、為什么需要DDD
- 當朋友和你聊工作時,你能否一語中的,說清你在開發(fā)中的業(yè)務內容及其價值?
- 當產品和你聊需求時,你是否遇到過反復溝通之后才發(fā)現(xiàn)講的不是同個東西的情況?
- 當你在做需求評估時,你是否經(jīng)常發(fā)現(xiàn)一個小的需求改動,總是牽一發(fā)動全身?
- 當你在快樂寫代碼時,你是否經(jīng)常覺得有些類可有可無,有些接口望文不知義?
如果你有以上的一些疑問,那你可以試試領域驅動設計:
DDD(Domain-driven design,領域驅動設計)是一種架構設計方法論,通過邊界劃分,將復雜業(yè)務領域簡單化,幫助我們設計出清晰的領域和應用邊界,保證業(yè)務模型與代碼模型的一致性。
在細看這個定義之前,我們可以思考一下,為什么我們的業(yè)務系統(tǒng)會慢慢變得復雜?
常見的情況是,業(yè)務在發(fā)展過程中為了探尋發(fā)力點,需要不斷地試錯迭代,調整方向,而系統(tǒng)在設計之初,難以預期到后面的瞬息萬變,為了應付業(yè)務,修修改改,久之,系統(tǒng)也變得復雜起來。
可以怎么辦呢?及時重構唄——不改變軟件系統(tǒng)外部行為的前提下,改善它的內部結構。
然而重構是從技術層面上抽煉出來的模型,往往不具有實際的業(yè)務含義,其他同學可能難以自然地將業(yè)務問題映射到對應的設計模型。另外,如果不能如實映射業(yè)務模型,隨著業(yè)務方向調整,代碼可能又開始腐敗……有點像芝諾悖論中,阿基里斯永遠追不上小烏龜。
那DDD怎么搞?
DDD是這么想的:”將業(yè)務架構映射到系統(tǒng)架構上,在響應業(yè)務變化調整業(yè)務架構時,也隨之變化系統(tǒng)架構”。可能大家平時有這樣的想法,但是比較模糊,未形成體系,而DDD就提供了一套完整的方法論。從業(yè)務角度去審視我們的系統(tǒng),從而實現(xiàn)高內聚低耦合的代碼。
整體而言,領域驅動設計包括戰(zhàn)略建模和戰(zhàn)術建模: 戰(zhàn)略設計側重于高層次、宏觀上去劃分和集成限界上下文,而戰(zhàn)術設計則關注更具體使用建模工具來細化上下文。
二、 如何實現(xiàn)DDD之戰(zhàn)略建模
1. 基本概念
1)領域、子域
在討論問題之前,我們需要先定義好問題。
領域即問題域,通常是根據(jù)一個組織所處的行業(yè)進行識別,它基于業(yè)務的愿景,定義了系統(tǒng)要解決的現(xiàn)實問題的目標和范圍。領域越大,業(yè)務的范圍也越大,大的領域可以拆分成小的問題域,稱之為子域。根據(jù)子域重要性和功能屬性劃,可以將其分為三類。
核心域、支撐域和通用域:
- 核心域:決定產品核心競爭力的子域
- 支撐域:實現(xiàn)核心域目標所需的,但重要程度不如核心域的子域,一般具備強烈的個性化需求
- 通用域:具有通用功能,可被多個子域使用的的是通用域。該子域所解決的問題一般是業(yè)界常見問題,有成熟的解決方案,可直接購買或簡單修改來使用
這個幾個概念其實很容易理解,不過在劃分的時候,注意要從業(yè)務的視角,而不是技術功能模塊來劃分。
2)限界上下文
我們語言博大精深,同樣的話在不同語境下就可演變出不同含義,這在溝通時總是帶來不必要的麻煩。為了準確地溝通,我們需要統(tǒng)一語言的邊界,在相同的語言邊界內溝通,才不容易出差錯。
一則阿凡提當理發(fā)師懲罰一個狡猾牧師的趣事:理發(fā)時,阿凡提刮臉時問牧師:“牧師,是否要眉毛?”牧師答:“這還用問,眉毛豈能不要?”.“好,你要我就給你!”,說著就把牧師的眉毛刮下來遞到他手里,牧師氣得說不出話來,誰叫自己說要呢。阿凡提又問:“牧師,胡子要嗎?”.“不要,不要!”牧師連忙說?!昂?,你不要就不要?!?嗖嗖幾刀就把牧師的胡子刮下來。
在一個系統(tǒng)中,一個名詞在不同語境可能有不同的含義,我們對它關注的屬性和行為也有所不同。例如,在電商系統(tǒng)中,對于產品Product, 在采購上下文,需要關注產品的進價、最小起訂量與供貨周期;在市場上下文中,則關心產品的品質、售價,以及用于促銷的精美圖片和銷售類型;在倉儲上下文中,倉庫工作人員更關心產品放在倉庫的哪個位置,產品的重量與體積,是否易碎品以及訂購產品的數(shù)量。
限界上下文在《實現(xiàn)領域驅動設計》中,用了很大篇幅去講,它有幾個重要的意義:
- 限界上下文是領域概念的語言邊界與業(yè)務邊界:在這個邊界內,領域概念的內涵是清晰、無歧義的
- 限界上下文是團隊的工作邊界:組織邊界與限界上下文對齊
- 限界上下文是技術方案的實施邊界:在這個邊界內,技術方案是獨立自治的,業(yè)務邏輯不會落入不同技術邊界的間隙
經(jīng)過戰(zhàn)略建模之后,我們可以得到以下的一個模型:
2. 業(yè)務實踐
為了更好地理解,我們對手上的一個項目:“IoT設備增值產品管理系統(tǒng)”進行實踐。該項目中,我們提供給商戶在IoT設備上管理增值運營產品的能力。這里的IoT設備主要是微信支付刷臉設備等。商戶可以在系統(tǒng)中創(chuàng)建我們業(yè)務中的增值運營產品,如電子海報、互動海報等,創(chuàng)建完之后,相關的增值產品會被投放到IoT設備上,進行展示、運作:
一開始我們從業(yè)務的用例出發(fā),認為我們的系統(tǒng)主要是商戶在我們頁面網(wǎng)站使用,以及IoT設備通過接口連接我們后臺服務,認為這兩個分屬不同的子域,然后梳理了一些支撐的功能:
畫完草圖之后,感覺不是很確定,于是便去咨詢部門的DDD專家王老師(十分感謝王立老師的指導),得到了一些寶貴的建議:我們應該避免直接從表現(xiàn)層去看業(yè)務,表現(xiàn)層就像是冰山露在水面上的棱角,這些棱角看起來毫不相干,但是實際上底層是連成一塊的,這些才是我們需要關注的。
就像這個項目,表面上商戶和設備是分開的,實際上它們在操作都是我們的增值運營產品,應該看成我們的系統(tǒng)提供統(tǒng)一對外的服務,然后商戶和設備來使用我們的服務。UGC內容存儲業(yè)務用例其實沒有涉及到的,屬于實現(xiàn)時候的東西。一番建議讓我們理清了思路,于是重新梳理,得到以下的戰(zhàn)略建模圖:
整體而言,我們將整體系統(tǒng)梳理為8個子域:
- 增值運營服務子域:核心域,是我們業(yè)務主要競爭力。從業(yè)務上來講,我們的核心是通過提供業(yè)務中IoT設備上的增值運營服務
- 增值運營產品子域:支撐域,這里主要是我們提供增值運營產品,如電子海報、互動海報等
- 生效場景子域:支撐域,業(yè)務中增值運營產品有不同生效場景,這里統(tǒng)一進行管理
- 準入子域:支撐域,現(xiàn)主要是業(yè)務中對使用者的一些限制規(guī)則
- 權限管理子域:支撐域,基于角色來管理使用者的權限
- 商戶信息子域:支撐域,提供商戶的信息
- IoT設備信息子域:支撐域,提供IoT設備的信息
- 風險識別子域:通用域,識別業(yè)務中一些安全風險,如不合規(guī)的UGC素材等。這部分是業(yè)界常見問題,可以使用通用方案來解決,實際上我們也是接入TEG的能力來實現(xiàn)
其中我們系統(tǒng)中的商戶信息依賴了微信支付商戶賬號信息和IoT設備鋪設服務信息,這里使用防腐層進行隔離,將外部的商戶信息“翻譯”為我們業(yè)務中的商戶信息。三、如何實現(xiàn)DDD之戰(zhàn)術建模梳理清楚上下文之間的關系后,我們基本了解業(yè)務的概貌,接下來需要細化上下文,進一步完善我們的模型。這里也需要用到DDD的一些基本概念。
3. 基本概念
1)實體、值對象
實體和值對象是組成領域模型的基礎單元。當一個對象由其標識(而不是屬性)區(qū)分時,這種對象稱為實體。如在校園教務系統(tǒng)中,每個賬戶是對應著一個學生,根據(jù)學號來唯一標識,可以認為是一個實體。傳統(tǒng)的數(shù)據(jù)建模大多是根據(jù)數(shù)據(jù)庫范式設計的,每一個數(shù)據(jù)庫表對應一個實體,每一個實體的屬性值用單獨的一列來存儲,一個實體主表會對應 N 個實體從表。
與其不同,DDD 是先構建領域模型,再將業(yè)務對象映射為持久化對象。這可能導致DDD建立出來的實體,映射到具體數(shù)據(jù)庫表時,可能是1對多,多對1的關系。
如一個賬戶實體,有它的基本信息和權限角色信息,可能就對應了2個持久化對象。另一方面,有時候為了某些查詢場景的方便,會把教師賬戶、學生賬戶等對應成一個持久化對象,就成了多對1。
通過對象屬性值來識別的對象,則可以認為是一個值對象。如地址信息{“省”: “廣東省”,”市”:”深圳市”},我們是通過它的屬性來區(qū)分出不同的地址。值對象實際上是想把一些不變的屬性組合起來,減少系統(tǒng)的復雜性。在設計值對象的時候,需要滿足以下的特性:
- 值對象相等性:可以通過對其屬性的比較,來區(qū)分不同的值對象
- 不變性:需要保證值對象創(chuàng)建后就不能被修改,即不允許外部再修改其屬性
- 可替換性:值對象是一個整體,當其描述的對象有變化時,需要用一個新的值對象來替換對于值對象,由于其具有不變性,且是通過屬性來判斷相等的,在設計對應的數(shù)據(jù)庫持久化對象時,可以將其以JSON形式存儲在數(shù)據(jù)庫表的某一字段中
2)聚合、聚合根
在 DDD 中,實體和值對象是基礎的領域對象。實體一般對應業(yè)務對象,它具有業(yè)務屬性和業(yè)務行為;而值對象主要是屬性集合,對實體的狀態(tài)和特征進行描述。但是我們的一個業(yè)務流程中,一般會同時涉及多個實體、值對象的操作,這里業(yè)務邏輯緊密的實體和值對象便組合成一個聚合。
從數(shù)據(jù)層面來看,同個聚合內的數(shù)據(jù)需要保持強一致性。
每一個聚合有一個聚合根實體,設置聚合根的主要目的是為了避免由于復雜數(shù)據(jù)模型缺少統(tǒng)一的業(yè)務規(guī)則控制,而導致聚合、實體之間數(shù)據(jù)不一致性的問題。聚合根可以看成是聚合的管理者,或是說handle。對內其協(xié)調實體和值對象完成業(yè)務邏輯。對外則提供通過聚合ID供其他聚合關聯(lián)引用,屏蔽外部對內部實體的直接訪問和修改。
建議的聚合設計原則:
- 在一致性邊界之內確保不變性:聚合用來封裝真正的不變性,而不是簡單地將對象組合在一起。聚合內有一套不變的業(yè)務規(guī)則,各實體和值對象按照統(tǒng)一的業(yè)務規(guī)則運行,實現(xiàn)對象數(shù)據(jù)的一致性。
- 設計小聚合:如果聚合聚合包含過多的實體,會提高管理實體的復雜性,高頻操作下容易并發(fā)沖突,降低了系統(tǒng)的性能。
- 在邊界之外使用最終一致性:不同的聚合之間不要求強一致性,保證最終一致性。一次事務操作中,只修改一個聚合實例,如果需要修改多個實例,可以考慮通過異步的方式保證最終一致性。
3)領域服務
領域服務的定義:領域中的服務表示一個無狀態(tài)的操作,它用于實現(xiàn)特定于某個領域的任務。當某個操作不適合放在聚合(實體)或值對像上時,最好的方式便是使用領域服務。
舉個例子,在一個路線導航的項目中,“路線”可能是其中的一個實體,如果業(yè)務中有“推薦路線上相關的美食”這樣一個功能,那我們會想,這個功能應該歸給哪個領域對象,給“路線”實體嗎?有點不合適,應該路線本身關注的是起終點,時間人物等。
此時可以將其這個功能歸為領域服務,它是一個路線狀態(tài)無關的服務,輸入路線各個節(jié)點,來得到沿路的各種美食。當然,要注意不要過度地使用領域服務,因為這很可能導致你把實體的行為都放在里面了,實體本身都變成了一些只有getter和setter的“貧血模型”。
4)領域事件
領域事件是領域模型中非常重要的一部分,用來表示領域中發(fā)生的事件。一個領域事件將導致進一步的業(yè)務操作,在實現(xiàn)業(yè)務解耦的同時,還有助于形成完整的業(yè)務閉環(huán)。
領域事件含義很廣泛,可以是業(yè)務流程的一個步驟,也可以是一個事件發(fā)生后觸發(fā)的后續(xù)動作,繳費完成之后,觸發(fā)短信通知;上面在設計聚合的時候,我們提到一個原則:在邊界之外使用最終一致性,一次事務最多只能更改一個聚合的狀態(tài)。如果一次業(yè)務操作涉及多個聚合狀態(tài)的更改,應通過領域事件,達到最終一致性。
實際上是通過事件驅動的這種異步方式,對系統(tǒng)進行解耦。當然,如果你覺得某兩個步驟,業(yè)務流程上不允許是不一致的,那就得重新考慮將其歸在同個聚合中了。
4. 業(yè)務實踐
我們以增值運營服務上下文為例,根據(jù)上面的理解,結合業(yè)務實際,得到以下模型:
其中增值產品是其中的一個聚合根,通過該聚合根進行各種領域操作。海報縮略圖是其中的一個領域服務,通過輸入產品素材中的海報url,來得到一個海報的縮略圖。
四、工程實踐
傳統(tǒng)的三層架構和DDD的分層結構:
在《領域驅動設計——軟件核心復雜性的應對之道》一書中,Eric提出了這樣的一種分層結構,將整個系統(tǒng)劃分為四層:用戶接口層、應用層、領域層和基礎設施層。
用戶接口層:用戶接口層負責向用戶顯示信息和解釋用戶指令。
應用層:應用層相對來說是較“薄”的一層,主要是部署了應用服務。應用服務的實現(xiàn)中,它負責編排和轉發(fā)下一層的領域層的接口,將要實現(xiàn)的功能委托給一個或多個領域對象來實現(xiàn),本身只負責處理業(yè)務用例的執(zhí)行順序以及結果的拼裝。
領域層:領域層是比較“厚”的一層,它包含聚合根、實體、值對象、領域服務等領域模型中的領域對象,實現(xiàn)了核心的業(yè)務邏輯。領域層和應用層的職責看起來有點模糊。
個人覺得,可以理解是應用層描述了一個具體操作從開始到結束的每一個環(huán)節(jié),而領域層則是對其的細化,用來處理具體的某一個環(huán)節(jié)。
比如,比如線上購物中,購物車結算這一場景可看成是一個應用行為。而這個行為又主要包括金額計算、支付、生成訂單,這些子環(huán)節(jié)就可以理解為一個領域層的服務了。
基礎設施層:可以看到上面三層都有箭頭指向基礎設施層,它的作用就是為其它各層提供通用的技術和基礎服務,如數(shù)據(jù)持久化、消息中間件等DDD 分層架構中的要素與傳統(tǒng)三層架構(用戶界面層、業(yè)務邏輯層、數(shù)據(jù)訪問層)還是挺相似的,一個主要的變化是將業(yè)務邏輯層的服務拆分到了應用層和領域層。應用層響應業(yè)務用例的變化,領域層關注不變的領域模型。
圖片來自極客時間
《DDD實戰(zhàn)課》在實際的代碼工程便是按照這樣的目錄來劃分,最近部門在推的整潔Git,也是這樣劃分目錄:
接下來,便是將領域對象映射到實際的類,實現(xiàn)對應的屬性和行為。當然,具體實現(xiàn)中有很多范式可參考和討論,我們也在摸索中,待后續(xù)慢慢補充……
五、總結
DDD首先不是關于技術的,而是關于討論、聆聽、理解、發(fā)現(xiàn)業(yè)務價值的?!猇aughn Vernon《實現(xiàn)領域驅動設計》
如Vernon所說的,DDD首先是關注業(yè)務的價值的。一開始我們對業(yè)務的邊界、目標可能有個大概了解,但是見解還是不盡相同。
通過一起對業(yè)務的討論與思考,我們了解了業(yè)務的概貌及核心,明確價值所在。關注到了核心,自然可以幫助我們實現(xiàn)與業(yè)務契合的系統(tǒng)。通過這次學習與實踐,我們進一步接觸了DDD。
當然,這也還只是開始,更多的關聯(lián)知識還隱藏在冰山之下。同時我們也明白,DDD也只是一種方法論上的參考,不是“銀彈”,需要不斷地去實踐與思考,才能體會出它的價值。
參考:
- Eric Evans.領域驅動設計.趙俐 盛海艷 劉霞等譯.人民郵電出版社,2016.
- 美團技術團隊.領域驅動設計在互聯(lián)網(wǎng)業(yè)務開發(fā)中的實踐:https://tech.meituan.com/2017/12/22/ddd-in-practice.html?spm=a2c4e.10696291.0.0.428119a4uu9Gpl
- 極客時間.DDD實戰(zhàn)課:https://time.geekbang.org/column/article/152677
作者:bryanzhao,微信支付后臺開發(fā)工程師
本文由 @騰訊大講堂 原創(chuàng)發(fā)布于人人都是產品經(jīng)理,未經(jīng)許可,禁止轉載。
題圖來自 Pixabay,基于CC0協(xié)議。
產品經(jīng)理在DDD中應該輸出哪些內容?領域劃分?由領域抽象出來的實體?
個人覺得DDD不應該替代代碼層的MVC結構,主要還是用來做產品和微服務架構的指導,用來劃分清楚系統(tǒng)模塊邊界
業(yè)務架構都已經(jīng)理解,但是關于技術方面的架構設計還是不太理解
同上,不適合小白
DDD,哈哈哈,我感覺因該叫3D,乍一眼還真的不知道要講什么,看完之后,果然不是干這個的,真的不怎么清楚。