領域驅動模型(DDD)

本文作者是組內同事 杜寧,目前負責美團外賣活動管理模塊業務。

什麼是領域驅動模型?

2004年Eric Evans 發表《領域驅動設計——軟件核心複雜性應對之道》(Domain-Driven Design –Tackling Complexity in the Heart of Software),簡稱Evans DDD,領域驅動設計思想進入軟件開發者的視野。領域驅動設計分為兩個階段:

  • 1、以一種領域專家、設計人員、開發人員都能理解的通用語言作為相互交流的工具,在交流的過程中發現領域概念,然後將這些概念設計成一個領域模型;

  • 2、由領域模型驅動軟件設計,用代碼來實現該領域模型;

簡單地說,軟件開發不是一蹴而就的事情,我們不可能在不瞭解產品(或行業領域)的前提下進行軟件開發,在開發前,通常需要進行大量的業務知識梳理,而後到達軟件設計的層面,最後才是開發。而在業務知識梳理的過程中,我們必然會形成某個領域知識,根據領域知識來一步步驅動軟件設計,就是領域驅動設計的基本概念。而領域驅動設計的核心就在於建立正確的領域驅動模型。

領域驅動模型(DDD)

image.png

傳統軟件開發與貧血模型

傳統的開發思想

傳統開發四層架構

領域驅動模型(DDD)

image.png

在傳統模型中,對象是數據的載體,只有簡單的getter/setter方法,沒有行為。以數據為中心,以數據庫ER設計作驅動。分層架構在這種開發模式下,可以理解為是對數據移動、處理和實現的過程。

以商家活動為例,首先設計數據庫表配置

領域驅動模型(DDD)

image.png

設計WmActPoi對象,只有簡單的get和set屬性方法

public class WmActPoi { private Long id; private String wmPoiId; private Integer startTime; private Integer endTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Integer getWmPoiId() { return wmPoiId; } public WmActPoiDB setWmPoiId(Integer wmPoiId) { this.wmPoiId = wmPoiId; } ......}

Service層代碼實現

class WmActPoiService { saveWmActPoi(); //保存活動 checkWmActPoi(); //活動校驗 ....} class WmActPoiQueryService { queryWmActPoi(); //查詢活動 queryWmActPoiByWmPoiId(); //根據門店查詢活動 ....}

可以看到,業務邏輯都是寫在Service中的,WmActPoi充其量只是個數據載體,沒有任何行為,是一種貧血模型。簡單的業務系統採用這種貧血模型和過程化設計是沒有問題的,但在業務邏輯複雜了,業務邏輯、狀態會散落到在大量方法中,原本的代碼意圖會漸漸不明確,我們將這種情況稱為由貧血症引起的失憶症。

領域驅動模型(DDD)

image.png

傳統架構的特點:

  • a. 以數據庫為中心

  • b. 貧血模型

  • c. 業務邏輯散落在大量的方法中

  • d. 當系統越來越複雜時,開發時間指數增長,維護成本很高

DDD設計思想

採用DDD的設計思想,業務邏輯不再集中在幾個大型的類上,而是由大量相對小的領域對象(類)組成,這些類具備自己的狀態和行為,每個類是相對完整的獨立體,並與現實領域的業務對象映射。領域模型就是由這樣許多的細粒度的類組成。

建立領域知識(Build Domain Model)

說了這麼多領域模型的概念,到底什麼是領域模型呢?作為一個軟件開發者,我們很難在對一個領域不瞭解的情況下著手開發,所以我們首先需要和領域專家溝通,建立領域只是。以飛機航行為例子:

現要為航空公司開發一款能夠為飛機提供導航,保證無路線衝突監控軟件。那我們應該從哪裡開始下手呢?根據DDD的思路,我們第一步是建立領域知識:作為平時管理和維護機場飛行秩序的工作人員來說,他們自然就是這個領域的專家,我們第一個目標就是與他們溝通,也許我們並不能從中獲取所有想要的知識,但至少可以篩選出主要的內容和元素。你可能會聽到諸如起飛,著陸,飛行衝突,延誤等領域名詞,讓們從一個簡單的例子開始:

  • 起點->飛機->終點

這個模型很直接,但有點過於簡單,因為我們無法看出飛機在空中做了什麼,也無法得知飛機怎麼從起點到的終點,那麼如此似乎會好些:

  • 飛機->路線->起點/終點

    既然點構成線,那何不:

  • 飛機->路線->points(含起點,終點)

    這個過程,是我們不斷建立領域知識的過程,其中的重點就是尋找領域專家頻繁溝通,從中提煉必要領域元素。

通用語言(Ubiquitous Language)

上面的例子的確看起來簡單,但過程並非容易:我們(開發人員)和領域專家在溝通的過程中是存在天然屏障的:我們滿腦子都是類,方法,設計模式,算法,繼承,封裝,多態,如何面向對象等等;這些領域專家是不懂的,他們只知道飛機故障,經緯度,航班路線等專業術語。

所以,在建立領域知識的時候,我們(開發人員和領域專家)必須要交換知識,知識的範圍範圍涉及領域模型的各個元素,如果一方對模型的描述令對方感到困惑,那麼應該立刻換一種描述方式,直到雙方都能夠接受並且理解為止。在這一過程中,就需要建立一種通用語言,作為開發人員和領域專家的溝通橋樑。

可如何形成這種通用語言呢?其實答案並不唯一,確切的說也沒有什麼標準答案。

(a) UML

利用UML可以清晰的表現類,並且展示它們之間的關係。但是一旦聚合關係複雜,UML葉子節點將會變的十分龐大,可能就沒有那麼直觀易懂了。最重要的是,它無法精確的描述類的行為。為了彌補這種缺陷,可以為具體的行為部分補充必要說明(可以是標籤或者文檔),但這往往又很耗時,而且更新維護起來十分不便。

(b) 文檔/繪圖

文檔耗時很長,可能不久就要變化,為模型從一開 始到它達到比較穩定的狀態會發生很多次變化, 可能在完成之前它們就已經作廢了。對於複雜系統,繪圖容易混亂。

(c) 偽代碼

極限編程推薦這麼做,但是使用難度大

領域驅動設計的分層架構和構成要素

領域驅動模型(DDD)

image.png

一個通用領域驅動設計的架構性解決方案包含4 個概念層:

領域驅動模型(DDD)

image.png

領域驅動模型(DDD)

image.png

層結構的劃分是很有必要的,只有清晰的結構,那麼最終的領域設計才宜用,比如用戶要預定航班,向Application Layer的service發起請求,而後Domain Layler從Infrastructure Layer獲取領域對象,校驗通過後會更新用戶狀態,最後再次通過Infratructure Layer持久化到數據庫中。

領域驅動模型的一些要素

領域驅動模型(DDD)

image.png

實體(Entity) & 值對象(Value Object)實體

與面向對象中的概念類似,在這裡再次提出是因為它是領域模型的基本元素。在領域模型中,實體應該具有唯一的標識符,從設計的一開始就應該考慮實體,決定是否建立一個實體也是十分重要的。值對象和我們說的編程中數值類型的變量是不同的,它僅僅是沒有唯一標識符的實體,比如有兩個收穫地址的信息完全一樣,那它就是值對象,並不是實體。值對象在領域模型中是可以被共享的,他們應該是“不可變的”(只讀的),當有其他地方需要用到值對象時,可以將它的副本作為參數傳遞。

服務(Services)

當我們在分析某一領域時,一直在嘗試如何將信息轉化為領域模型,但並非所有的點我們都能用Model來涵蓋。對象應當有屬性,狀態和行為,但有時領域中有一些行為是無法映射到具體的對象中的,我們也不能強行將其放入在某一個模型對象中,而將其單獨作為一個方法又沒有地方,此時就需要服務服務是無狀態的,對象是有狀態的。所謂狀態,就是對象的基本屬性:高矮胖瘦,年輕漂亮。服務本身也是對象,但它卻沒有屬性(只有行為),因此說是無狀態的。

服務存在的目的就是為領域提供簡單的方法。為了提供大量便捷的方法,自然要關聯許多領域模型,所以說,行為(Action)天生就應該存在於服務中。

服務具有以下特點:

  • a)服務中體現的行為一定是不屬於任何實體和值對象的,但它屬於領域模型的範圍內

  • b)服務的行為一定涉及其他多個對象

  • c)服務的操作是無狀態的

模塊(Moudles)

對於一個複雜的應用來說,領域模型將會變的越來越大,以至於很難去描述和理解,更別提模型之間的關係了。模塊的出現,就是為了組織統一的模型概念來達到減少複雜性的目的。而另一個原因則是模塊可以提高代碼質量和可維護性,比如我們常說的高內聚,低耦合就是要提倡將相關的類內聚在一起實現模塊化。

模塊應當有對外的統一接口供其他模塊調用,比如有三個對象在模塊a中,那麼模塊b不應該直接操作這三個對象,而是操作暴露的接口。模塊的命名也很有講究,最好能夠深層次反映領域模型。

聚合(Aggregates)

聚合表示一組領域對象(包括實體和值對象),用來表述一個完整的領域概念。而每個聚合都有一個根實體,這個根實體又叫做聚合根。舉個簡單的例子,一個電腦包含硬盤、CPU、內存條等,這一個組合就是一個聚合,而電腦就是這個組合的聚合根。在聚合中,根是唯一允許外部對象保持對它的引用的元素,而邊界內部的對象之間則可以互相引用。除根以外的其他Entity都有本地表示,但這些標識只有在聚合內部才需要加以區別,因為外部對象除了根Entity之外看不到其他對象。

領域驅動模型(DDD)

image.png

工廠(Factory)

一個對象的創建可能是它自身的主要操作,但是複雜的組裝操作不應該成為被創建對象的職責。組合這樣的職責會產生笨拙的設計, 也很難讓人理解。因此,有必要引入一個新的概念,這個概念可以幫助封裝複雜的對象創建過程,它就是工廠(Factory)。工廠用來封裝對象創建所必需的知識,它們對創建聚合特別有用。當聚合的根建立時,所有聚合包含的對象將隨之建立。

領域驅動模型(DDD)

image.png

資源庫(Repositories)

資源庫的是封裝所有獲取對象引用所需的邏輯。領域對象不需處理基礎設施,以得到領域中對其他對象的所需的引用。只需從資源庫中獲取它們,於是模型重獲它應有的清晰和焦點。

資源庫會保存對某些對象的引用。當一個對象被創建出來時,它可以被保存到資源庫中,然後以後使用時可從資源庫中檢索到。如果客戶程序從資源庫中請求一個對象,而資源庫中並沒有它,就會從存儲介質中獲取它。換種說法是,資源庫作為一個全局的可訪問對象的存儲點而存在。

Repository的接口應當採用領域通用語言。作為客戶端,不應當知道數據庫實現的細節。

Repository和DAO的作用類似,二者的主要區別:

DAO是比Repository更低的一層,包含了如何從數據庫中提取數據的代碼。

Repository以“領域”為中心,所描述的是“領域語言”。Repository把ORM框架與領域模型隔離,對外隱藏封裝了數據訪問機制。

領域驅動模型(DDD)

image.png

publicinterface AccountRepository { Account findAccount(String accountId); void addAccount(Accountaccount);}

工廠和資源庫之間存在一定的關係。它們都是模型驅動設計中的模式,它們都能幫助我們關聯領域對象的生命週期。然而工廠關注的是對象的創建,而資源庫關心的是已經存在的對象。資源庫可能會 在本地緩存對象,但更常見的情況是需要從一個持久化存儲中檢索 它們。對象可以用構造函數創建,也可以被傳遞給一個工廠來構 建。從這個原因上講,資源庫也可以被看作一個工廠,因為它創建對象。不過它不是從無到有創建新的對象,而是對已有對象的重建。我們將不把資源庫視為一個工廠。工廠創建新的對象,而資源庫應該是用來發現已經創建過的對象。當一個新對象被添加到資源庫時,它應該是先由工廠創建過的,然後它應該被傳遞給資源庫以便將來保存它,見下面的例子:

領域驅動模型(DDD)

image.png

持續集成與模型一致性

規約(Factory)

規約是一種布爾斷言。

領域驅動模型(DDD)

image.png

規約是業務規則的 部分 理論上規約類中的方法只有個:isSatisfiedBy(Object obj)。

規約用來測試對象是否滿足某種條件,用來進行對象查詢,也可以作為某個對象的創建條件。

單一規約規則。多個規約通過組合表現複雜的規約。

領域驅動模型(DDD)

image.png

領域驅動模型(DDD)

image.png

領域驅動模型(DDD)

image.png

領域驅動模型(DDD)

image.png

限界上下文(Bounded Context)

明確的定義模型所應用的上下文。根據團隊的組織、軟件系統的功能和物理表現(代碼數據庫)來設置模型的邊界。在這些邊界中嚴格保持模型的一致性,而不要受到邊界之外問題的混淆。每個團隊負責自己的模型,併為其他模型提供服務。

上下文映射(Context Map)

一個企業應用有多個模型,每個模型有自己的界定的上下文。建議用上下文作為團隊組織的基礎。在同一個團隊裡的人們能更容易地 溝通,也能很好地將模型集成和實現。但是每個團隊都工作於自己 的模型,所以最好讓每個人都能瞭解所有的模型。上下文映射(Context Map)是指抽象出不同界定上下文和它們之間關係的文 檔,它可以是像下面所說的一個試圖(Diagram),也可以是其他任 何寫就的文檔。詳細的層次各有不同。它的重要之處是讓每個在項 目中工作的人都能夠得到並理解它。

領域驅動模型(DDD)

image.png

共享內核(Shared Kernel)

領域驅動模型(DDD)

image.png

總結

領域驅動設計的核心是領域模型,這一方法論可以通俗的理解為先找到業務中的領域模型,以領域模型為中心驅動項目的開發。而領域模型的設計精髓在於面向對象分析,在於對事物的抽象能力,一個領域驅動架構師必然是一個面向對象分析的大師。

在面向對象編程中講究封裝,講究設計低耦合,高內聚的類。而對於一個軟件工程來講,僅僅只靠類的設計是不夠的,我們需要把緊密聯繫在一起的業務設計為一個領域模型,讓領域模型內部隱藏一些細節,這樣一來領域模型和領域模型之間的關係就會變得簡單。這一思想有效的降低了複雜的業務之間千絲萬縷的耦合關係。

DDD開發案例

超市收銀業務

領域驅動設計在互聯網業務開發中的實踐

本文作者是組內同事 杜寧,目前負責美團外賣活動管理模塊業務。


個人介紹:

高廣超:多年一線互聯網研發與架構設計經驗,擅長設計與落地高可用、高性能、可擴展的互聯網架構。

本文首發在 高廣超的簡書博客 轉載請註明!


分享到:


相關文章: