1. 程式人生 > 其它 >DDD(領域驅動設計)

DDD(領域驅動設計)

什麼是DDD

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

聽起來這和傳統意義的軟體開發沒啥區別,只是換了點新鮮的名詞而已,其實不然。

軟體開發 VS DDD

一般軟體設計或者說軟體開發分兩種:瀑布式敏捷式

前者一般是專案經理經過大量的業務分析後,會基於現有需求整理出一個基本模型,再將結果傳遞給開發人員,這就是開發人員的需求文件,他們只需要照此開發便是。這種模式下,是很難頻繁的從使用者那裡得到反饋,因此在前期分析時就已經默認了這個業務模型是正確的,那麼結果可想而之,數月甚至數年後交付的時候,必然和客戶的預期差距較大。

後者在此基礎上進行了改進,它也需要大量的分析,範圍會設計到更精細的業務模組,它是小步迭代,週期性交付,那麼獲取客戶的反饋也就比較頻繁和及時。可敏捷也不能夠將業務中的方方面面都考慮到,並且敏捷是擁抱變化的,大量的需求或者業務模型變更必將帶來不小的維護成本,同時,對人(Developer)的要求也必然會更高。

DDD則不同:它像是更小粒度的迭代設計,它的最小單元是領域模型(Domain Model),所謂領域模型就是能夠精確反映領域中某一知識元素的載體,這種知識的獲取需要通過與領域專家(Domain Expert)進行頻繁的溝通才能將專業知識轉化為領域模型。領域模型無關技術,具有高度的業務抽象性,它能夠精確的描述領域中的知識體系;同時它也是獨立的,我們還需要學會如何讓它具有表達性,讓模型彼此之間建立關係,形成完整的領域架構。通常我們可以用象形圖或一種通用的語言(Ubiquitous Language)

去描述它們之間的關係。在此之上,我們就可以進行領域中的程式碼設計(Domain Code Design)。如果將軟體設計比做是造一座房子,那麼領域程式碼設計就好比是貼桌布。前者已經將房子的藍圖框架規劃好,而後者只是一個小部分的設計:如果牆紙貼錯了,我們可以重來,可如果房子結構設計錯了,那可就悲劇了。

建立領域知識(Build Domain Model)

說了這麼多領域模型的概念,到底什麼是領域模型呢?以飛機航行為例子:

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

  • 起點->飛機->終點

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

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

既然點構成線,那何不:

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

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

儘管看起來還是很簡單,但我們已經開始一步步的在建立領域物件和領域模型了。

通用語言(Ubiquitous Language)

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

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

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

a)UML
利用UML可以清晰的表現類,並且展示它們之間的關係。但是一旦聚合關係複雜,UML葉子節點將會變的十分龐大,可能就沒有那麼直觀易懂了。最重要的是,它無法精確的描述類的行為。為了彌補這種缺陷,可以為具體的行為部分補充必要說明(可以是標籤或者文件),但這往往又很耗時,而且更新維護起來十分不便。
b)虛擬碼
極限程式設計是推薦這麼做的,這個辦法對程式猿來說固然好,可立刻就要將現有模型對映到程式碼層面,這對人的要求也是不低,並不容易實現。

模型驅動設計(Domain Driven Design)

模型關係圖(Model-Driven Design)

領域驅動設計中的模型關係圖如下:

層結構(Layered Architecture)

  • User Interface

負責向用戶展現資訊,並且會解析使用者行為,即常說的展現層。

  • Application Layer

應用層沒有任何的業務邏輯程式碼,它很簡單,它主要為程式提供任務處理。

  • Domain Layer

這一層包含有關領域的資訊,是業務的核心,領域模型的狀態都直接或間接(持久化至資料庫)儲存在這一層。

  • Infrastructure Layer

為其他層提供底層依賴操作。

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

實體(Entity) & 值物件(Value Object)

實體與面向物件中的概念類似,在這裡再次提出是因為它是領域模型的基本元素。在領域模型中,實體應該具有唯一的識別符號,從設計的一開始就應該考慮實體,決定是否建立一個實體也是十分重要的。

值物件和我們說的程式設計中數值型別的變數是不同的,它僅僅是沒有唯一識別符號的實體,比如有兩個收穫地址的資訊完全一樣,那它就是值物件,並不是實體。值物件在領域模型中是可以被共享的,他們應該是“不可變的”(只讀的),當有其他地方需要用到值物件時,可以將它的副本作為引數傳遞。

服務(Services)

當我們在分析某一領域時,一直在嘗試如何將資訊轉化為領域模型,但並非所有的點我們都能用Model來涵蓋。物件應當有屬性,狀態和行為,但有時領域中有一些行為是無法對映到具體的物件中的,我們也不能強行將其放入在某一個模型物件中,而將其單獨作為一個方法又沒有地方,此時就需要服務.

服務是無狀態的,物件是有狀態的。所謂狀態,就是物件的基本屬性:高矮胖瘦,年輕漂亮。服務本身也是物件,但它卻沒有屬性(只有行為),因此說是無狀態的。

PS:這與我們常說的伺服器的狀態是兩個概念,無狀態的伺服器是指,對伺服器來說每次接收到的HTTP請求都像是客戶端第一次傳送的一樣;而有狀態的伺服器就會儲存客戶端的狀態,常見的就是Cookie&Session

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

服務具有以下特點:

a)服務中體現的行為一定是不屬於任何實體和值物件的,但它屬於領域模型的範圍內
b)服務的行為一定設計其他多個物件
c)服務的操作是無狀態的

PS:不要隨意放置服務,如果該行為是屬於應用層的,那就應該放在那;如果它為領域模型服務,那它就應該儲存在領域層中,要避免業務的服務直接操作資料庫,最好通過DAO。

模組(Moudles)

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

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

聚合(Aggregates)

聚合被看作是多個模型單元間的組合,它定義了模型的關係和邊界。每個聚合都有一個根,根是一個實體,並且是唯一可被外訪問的。正是如此,聚合可以保證多個模型單元的不變性,因為其他模型都參考聚合的根。所以要想改變其他物件,只能通過聚合的根去操作。根如果沒有了,那麼聚合中的其他物件也將不存在。
一個簡單的例子如下:

customer是該聚合的根,其他的都是內部物件,如果外部需要使用者地址,拷貝一份傳遞出去即可。顯而易見,使用者如果不存在,其他資訊均無意義。

工廠(Factories)

在大型系統中,實體和聚合通常是很複雜的,這就導致了很難去通過構造器來建立物件。工廠就決解了這個問題,它把建立物件的細節封裝起來,巧妙的實現了依賴反轉。當然對聚合也適用(當建立了聚合根時,其他物件可以自動建立)。工廠最早被大家熟知可能還是在設計模式中,的確,在這裡提到的工廠也是這個概念。

但是不要盲目的去應用工廠,以下場景不需要工廠:
a)構造器很簡單
b)構造物件時不依賴於其他物件的建立
c)用策略模式就可以解決

倉庫(Repository)

倉庫封裝了獲取物件的邏輯,領域物件無須和底層資料庫互動,它只需要從倉庫中獲取物件即可。倉庫可以儲存物件的引用,當一個物件被建立後,它可能會被儲存到倉庫中,那麼下次就可以從倉庫取。如果使用者請求的資料沒在倉庫中,則會從資料庫裡取,這就減少了底層互動的次數。當然,倉庫獲取物件也是有策略的,如下:

PS:倉庫看起來有些像Infrastructure Layer的東西,但其實不然,倉庫更像是本地快取,需要時才會訪問資料庫

結束語

CQRS本身也是一種架構模式,但更多的是它被應用在DDD中。因為DDD中有工廠倉庫來管理領域模型,前者主要用於建立,而後者則用於儲存。這就表明在DDD中是預設將讀寫分離的,DDD似乎就天生和CQRS有著無縫的連結。

CQRS往往要求資料庫進行讀寫分離,具體來說,所有的更新操作均無返回值(void),而讀操作才返回對應的值。在實現CQRS時,又和事件源(Event Source)相結合,以下是一個簡單的互動過程:

客戶端發起一個請求,服務端將其對映為一個命令,該命令會從倉庫中讀取一個相關的聚合,對該聚合進行操作,將會生成一個事件源,將該事件傳送出去,接收方收到訊息後(並不是立刻)將會更新領域物件,完成一次更新操作。

在此基礎上,還有稱之為六邊形的架構風格,它將DDD的領域模型包裹在內,外圍含有多種介面卡來適配各種通訊方式,總體來說,我覺得無論是DDD,CQRS還是六邊形,都是一種架構的設計思路,沒有絕對的優勢,同時也有各自的複雜度,並不容易理解,但有時在軟體設計時,不妨多學習一下其中的小細節和思路,必然能夠有所收穫。

至於能否應用?如何應用?,筆者只能說不能生搬硬套,需要有一定的實踐經驗才能去嘗試,一般情況下,結合專案特點,能適當的靈活採用其中的設計思路即可。

參考資料:

連結:https://www.jianshu.com/p/b6ec06d6b594 作者:Pursue