DDD(Domain-Driven Design)領域驅動架構介紹.md

1. 什麼是領域模型

在理解領域模型之前,我們先思考一下軟件開發的本質是什麼。從本質上來說,軟件開發過程就是問題空間解決方案空間的一個映射轉化,如圖1所示。
在這裏插入圖片描述
在問題空間中,我們主要是找出某個業務面臨的挑戰及其相關需求場景用例分析;而在解決方案空間中,則通過具體的技術工具手段來進行設計實現。

就軟件系統來說,“問題空間”就是系統要解決的“領域問題”。因此,也可以簡單理解爲一個領域就對應一個問題空間,是一個特定範圍邊界內的業務需求的總和。

“領域模型”就是“解決方案空間”,是針對特定領域裏的關鍵事物及其關係的可視化表現,是爲了準確定義需要解決問題而構造的抽象模型,是業務功能場景在軟件系統裏的映射轉化,其目標是爲軟件系統構建統一的認知。

例如,請假系統解決的是人力工時的問題,屬於人力資源領域,對口的是HR部門;費用報銷系統解決的是員工和公司之間的財務問題,屬於財務領域,對口的是財務部門;電商平臺解決的是網上購物問題,屬於電商領域。可以看出,每個軟件系統本質上都解決了特定的問題,屬於某一個特定領域,實現了同樣的核心業務功能來解決該領域中核心的業務需求。

2. 什麼是DDD

DDD是Eric Evans在2003年出版的《領域驅動設計:軟件核心複雜性應對之道》(Domain-Driven Design: Tackling Complexity in the Heart of Software)一書中提出的具有劃時代意義的重要概念,是指通過統一語言、業務抽象、領域劃分和領域建模等一系列手段來控制軟件複雜度的方法論.

DDD的革命性在於領域驅動設計是面向對象分析的方法論,它可以利用面向對象的特性(封裝、多態)有效地化解複雜性,而傳統J2EE或Spring+Hibernate等事務性編程模型只關心數據。這些數據對象除了簡單的setter/getter方法外,不包含任何業務邏輯,業務邏輯都是以過程式的代碼寫在Service中。這種方式極易上手,但隨着業務的發展,系統也很容易變得混亂複雜。

領域驅動設計關心的是業務中的領域劃分(戰略設計)和領域建模(戰術設計),其開發過程不再以數據模型爲起點,而是以領域模型爲出發點,研發過程如圖2所示。領域模型對應的是業務實體,在程序中主要表現爲類、聚合根和值對象,它更加關注業務語義的顯性化表達,而不是數據的存儲和數據之間的關係

在這裏插入圖片描述

3. DDD的優勢

3.1 統一語言

統一語言(Ubiquitous Language)的主要思想是讓應用能和業務相匹配,這是通過在業務與代碼中的技術之間採用共同的語言達成的。
業務語言起源於公司的業務側,業務側擁有需要實現的概念。業務語言中的術語由公司的的業務側和技術側通過協商來定義(意味着業務側也不能總是選到最好的命名),目標是創造可以被業務、技術和代碼自身無歧義使用的共同術語,即統一語言。代碼、類、方法、屬性和模塊的命名必須和統一語言相匹配,必要的時候需要對代碼進行重構!
試想,在PRD文檔、設計文檔、代碼以及團隊日常交流中,如果有一套領域術語是統一無歧義的,是否會極大地提升溝通和工作效率?在日常工作中,因爲概念理解不一致,或者語言表達上的問題,導致溝通效率低,甚至發生誤解的情況實在太多了。所以,明確概念、形成統一語言至關重要。

3.2 面向對象

DDD的核心是領域模型,這一方法論可以通俗地理解爲先找到業務中的領域模型,以領域模型爲中心,驅動項目開發。
領域模型的設計精髓在於``面向對象分析、對事物的抽象能力,一個領域驅動架構師必然是一個面向對象分析的大師。 DDD鼓勵我們接觸到需求後第一步就是考慮領域模型,而不是將其切割成數據和行爲,然後用數據庫實現數據,用服務實現行爲,最後造成需求的首尾分離。 DDD會讓你首先考慮業務語言,而不是數據。DDD強調業務抽象和麪向對象編程,而不是過程式業務邏輯實現。重點不同,導致編程世界觀不同```。

建模本質上是一種抽象。抽象就是歸類,其目的是減輕認知的負擔,避免重複的思考和工作,提升人的計算能力。

3.3 業務語義顯性化

統一語言也好,面向對象也好,最終的目都是爲代碼的可讀性和可維護性服務。統一語言使得我們的核心領域概念可以無損地在代碼中呈現,從而提升代碼的可理解性。

面向對象也是讓代碼儘量體現領域實體和實體之間的關係原貌,所以目的也是業務語義被顯性化地表達,顯性化的結果是代碼更容易被理解和維護,殊途同歸,一切都是爲了控制複雜度。在軟件的世界裏,任何的方法論如果最終不能落在“減少代碼複雜度”這個焦點上,那麼都是有待商榷的。

3.4 分離業務邏輯和技術細節

代碼複雜度是由業務複雜度和技術複雜度共同組成的。實踐DDD還有一個好處,是讓我們有機會分離核心業務邏輯和技術細節,讓兩個維度的複雜度有機會被解開和分治。如圖3所示,核心業務邏輯是整個應用的核心,最好只是簡單Java類(Plan Old Java Object,POJO)。也就是說,核心業務邏輯對技術細節沒有任何依賴,依賴都是由外向內的,即使有由內向外的依賴,也應該通過依賴倒置來反轉依賴的方向。通過這樣的劃分,Entities只要安安心心地處理業務邏輯就好,業務邏輯越複雜,這樣劃分帶來的好處越明顯。
在這裏插入圖片描述

4. DDD核心概念

4.1 領域實體

毫不誇張地說,我們的軟件系統就是對現實世界的真實模擬。如圖4所示,現實世界中的事物在軟件世界中可以被模擬成一個對象:該事物在現實世界中被賦予什麼職責,在軟件世界中就被賦予什麼職責;在現實世界中擁有什麼特性,在軟件世界中就擁有什麼屬性;在現實世界中擁有什麼行爲,在軟件世界中就擁有什麼函數;在現實世界中與哪些事物存在怎樣的關係,在軟件世界中就應當與它們發生怎樣的關聯。這正是面向對象編程的核心思想,也是DDD中尋找領域實體的核心思想。
在這裏插入圖片描述

4.2 聚合根

聚合根(Aggregate Root)是DDD中的一個概念,是一種更大範圍的封裝,會把一組有相同生命週期、在業務上不可分割的實體和值對象放在一起,只有根實體可以對外暴露引用,這也是一種內聚性的表現。
仍以銀行轉賬的例子來說明,如圖5所示,賬號(Account)是客戶信息(CustomerInfo)Entity和值對象(Address)的聚合根,交易(Tansaction)是流水(Journal)的聚合根,流水是因爲交易才產生的,具有相同的生命週期。
在這裏插入圖片描述

4.3 領域服務

有些領域中的動作是一些動詞,看上去並不屬於任何對象。它們代表了領域中的一個重要的行爲,所以不能忽略它們或者簡單地把它們合併到某個實體或者值對象中。當這樣的行爲從領域中被識別出來時,推薦的實踐方式是將它聲明成一個服務。這樣的對象不再擁有內置的狀態,其作用僅僅是爲領域提供相應的功能。Service往往是以一個活動來命名,而不是Entity來命名。

例如在銀行轉賬的例子中,轉賬(transfer)這個行爲是一個非常重要的領域概念,但是它發生在兩個賬號之間,歸屬於賬號Entity並不合適,因爲一個賬號Entity沒有必要去關聯它需要轉賬的賬號Entity。在這種情況下,使用MoneyTransferDomainService就比較合適了。識別領域服務,主要看它是否滿足以下3個特徵。
(1)服務執行的操作代表了一個領域概念,這個領域概念無法自然地隸屬於一個實體或者值對象。
(2)被執行的操作涉及領域中的其他對象。
(3)操作是無狀態的。

4.4 領域事件

領域事件(Domain Event)是在一個特定領域由一個用戶動作觸發的,是發生在過去的行爲產生的事件,而這個事件是系統中的其他部分或者關聯繫統感興趣的。
爲什麼領域事件如此重要?因爲在分佈式環境下,很少有業務系統是單體的(Monolithic),消息作爲分佈式系統間耦合度最低、最健壯、最容易擴展的一種通信機制,是我們實現分佈式系統互通的重要手段。關於領域事件,我們需要注意兩點,分別是事件命名和事件內容。

1.事件命名
事件是表示發生在過去的事情,所以在命名上推薦使用Domain Name + 動詞的過去式 + Event,這樣可以更準確地表達業務語義。
例如,在銀行轉賬的例子中,對於轉賬成功和失敗我們都需要發出事件通知,可以定義兩個領域事件如下。
(1)MoneyTransferedEvent:表示轉賬成功發出的事件。
(2)MoneyTransferFailedEvent:表示轉賬失敗發出的事件。

2.事件內容
事件內容在計算機術語中叫作payload,有以下兩種形式。
(1)自恰(Enrichment):就是在事件的payload中儘量多放數據,這樣consumer不需要回查就能處理消息,也就是自恰地處理消息。
(2)回查(Query-Back):這種方式是隻在payload放置id屬性,然後consumer通過回調的形式獲取更多數據。這種形式會加重系統的負載,可能會引起性能問題。

4.5 邊界上下文

領域實體的意義是有上下文的,比如同樣是Apple,在水果店和蘋果手機專賣店中表達出的含義就完全不一樣。邊界上下文(Bounded Context)的作用是限定模型的應用範圍,在同一個上下文中,要保證模型在邏輯上統一,而不用考慮它是不是適用於邊界之外的情況。
那麼不同上下文之間的業務實體要如何實現交互呢?就像關係數據庫和對象之間需要ORM一樣,不同上下文之間的實體也需要映射。在DDD中,這種機制叫作上下文映射(Context Mapping),我們可以使用防腐層(Anti-Corruption)來完成映射的工作。
如圖6所示,在我們開發的CRM系統中,商家的客戶大部分是來自於ICBU網站的會員,雖然二者有很多屬性都是一樣的,但我們還是有必要引入防腐層來做上下文映射,主要有以下兩個原因。
(1)雖然屬性大部分一樣,但二者的作用和行爲在各自上下文中是不一樣的。
(2)解耦影響,加入了防腐層之後,網站的會員變化就不會影響到CRM系統了。
在這裏插入圖片描述#### 5. 領域模型設計:基於對象vs基於數據庫

  • a. Data Modeling:通過數據抽象系統關係,也就是數據庫設計
  • b. Object Modeling:通過面向對象方式抽象系統關係,也就是面向對象設計

基於數據庫,
在service層通過我們非常喜歡的manager去manage大部分的邏輯,POJO(稍後章節裏的失血模型)作爲數據在manager手(上帝之手)裏不停地變換和組合,service層在這裏是一個巨大的加工工廠(很重的一層),圍繞着數據庫這份DNA,完成業務邏輯。
在這裏插入圖片描述
假設你的機器內存無限大,永遠不宕機,在這個前提假設下,我們是不需要持久化數據的,也就是我們可以不需要數據庫,那麼你將會怎麼設計你的軟件?這就是我們說的Persistence Ignorance:持久化無關設計
沒了數據庫,領域模型就要基於程序本身來設計了,熱愛設計模式的同學們可以在這裏大顯身手。在面向過程,面向函數,面向對象的編程語言中,面向對象無疑是領域建模最佳方式。

類與表有點像

領域模型並不完成業務,每個domain object都是完成屬於自己應有的行爲(single responsibility),就如同人跑這個動作,person.run是一個與業務無關的行爲,但這個時候manger或者service在調用 some person.run的時候可能完成的100米比賽這個業務,也可能是完成跑去送外賣這個業務。這樣的話形成了類似於如下的架構圖:
在這裏插入圖片描述
我們回到假設,假設你的機器內存無限大,永遠不宕機,現在把假設去掉,沒有誰的機器是內存無限大,永遠不宕機的。去掉這個假設,我們需要數據庫,但數據庫的職責不再承載領域模型這個沉重的包袱了,數據庫迴歸persistence的本質,完成以下兩個事情:

  • 【存】將對象數據持久化到存儲介質中
  • 【取】高效地把數據查詢返回到內存中
    在這裏插入圖片描述

領域模型是用於領域操作的,當然也可以用於查詢(read),不過這個查詢是代價的。在這個前提下,一個aggregate可能內含了若干數據,這些數據除了類似於getById這種方式,不適用多樣化查詢(query),領域驅動設計也不是爲多樣化查詢設計的。

查詢是基於數據庫的,所有的複雜變態查詢其實都應該繞過Domain層,直接與數據庫打交道

再精簡一下:````領域操作->objects, 數據查詢->table rows。```

6. 領域模型設計:業務對象的抽象

在開始進行抽象前一個必須的步驟就是“講故事”!

講什麼故事呢?關於這個子域解決的業務問題或者提供的業務能力的故事。既然是故事,就必須有清晰的業務場景和業務對象之間的交互。這件事情看起來是如此自然和簡單,然則一個團隊裏能夠站起來有條不紊陳述清楚的卻沒有幾人。讀到這裏的讀者不妨停下來試試,你是否能夠把現在你所做的業務在兩三分鐘內場景化地描述出來?
這麼做顯然目的是讓我們能夠比較完整地思考我們所要提煉和抽象的業務對象有哪些。只有當我們能夠“講”清楚業務場景的時候,才應該開始抽象的步驟。對於一個業務對象,我們常見的抽象可以是“實體”(Entity)“值對象”(Value Object)

7. 如何進行領域建模

7.1 用例分析法

用例分析法是進行領域建模最簡單可行的方式。

在這裏插入圖片描述

7.2 DDD的方法

Eric Evans的著作Domain-Driven Design領域驅動設計,簡稱DDD。DDD是一套軟件開發方法論,用來解決複雜的現實問題。

  • 構建領域知識
  • 創建通用語言
  • 團隊在進行所有方式的溝通時(文字,演講,圖形)都需要採用這種一致的語言。
  • 創建實體
    -
  • 創建值對象
    在這裏插入圖片描述
  • 創建聚合根
    在這裏插入圖片描述
7.3 四色建模法

四色建模法源於《Java Modeling In Color With UML》[10],它是一種模型的分析和設計方法,通過把所有模型分爲四種類型,幫助模型做到清晰、可追溯。
在這裏插入圖片描述

8.實踐

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章