領域驅動設計(DDD)入門&概要

永遠_不會懂發表於2020-10-19

我們為什麼需要領域驅動設計

在說什麼是領域驅動設計之前,我覺得需要先說一下我們為什麼需要領域驅動,我個人認為領域驅動設計對於研發來說改進點主要有下面三個:

  1. 從大泥球風格中解脫出來,控制程式碼複雜度
  2. 迴歸物件導向程式設計本質,而不是程式導向程式設計
  3. 專注於業務,實現不同業務領域解偶

什麼是領域驅動設計

領域驅動設計(DDD)是一種軟體設計思路,領域指的是業務領域,比如銀行業務領域,醫藥銷售領域;不同於傳統以資料表為中心的建模方式,它以業務領域為中心來建模,能促使我們以正確的方式使用物件導向,建立飽滿的領域物件。

在進行領域建模和開發時我們主要需要關注以下幾點:

  1. 和領域專家一起構建一套通用語言,團隊成員使用通用業務語言進行交流;
  2. 關注領域的戰略設計,DDD的戰略設計從更高層次抽象系統,劃分出不同的系統和業務關注點,戰略設計包含:領域/子域劃分、通用語言的構建、限界上下文定義以及架構風格選擇等;
  3. 實現DDD的戰術設計,戰術設計主要包含:領域物件(實體、值物件、聚合)、模組、領域服務、工廠等;

下面的思維導圖是領域驅動設計需要掌握的一些點:

 

Tips:

1.領域驅動設計是2004年Eric Evans在《領域驅動設計:軟體核心複雜性應對之道》這本書中提到的,DDD的出現是為了應對複雜業務場景下軟體越來越複雜問題;也就是說通過DDD可以將軟體的複雜度控制在合理的範圍內。

2.領域驅動設計主要解決的是業務複雜度問題(避免大泥球風格:大泥球風格就是沒有任何清楚的結構,例如隨意共享的資料,隨意全域性化的資料結構。這樣風格的系統可維護性(maintainability)和可擴充套件性(extensibility)都很差,最終導致整個系統難以改動,維護不下去),如果業務不復雜,則不需要使用DDD方式來處理(推薦用三層架構)。

領域驅動設計的優缺點

領域驅動設計的優缺點很明顯,我這邊整理了幾個優缺點,供參考。

優點:

  1. 由領域專家進行領域模型設計,通過業務驅動開發而不是資料庫設計來驅動專案開發,業務邏輯更加清晰;
  2. 再次強調了物件導向程式設計的重要性,減少程式導向程式設計(提高內聚性、降低上下文之間耦合);
  3. 對單元測試(自測)比較友好;

缺點:

  1. 需要有對DDD比較精通的人員進行領域的建模,如果建模不當,後續會比較麻煩;
  2. 對開發人員要求比較高,開發人員需要理解DDD才能進行開發;
  3. DDD本身概念性的東西比較多,有些概念不好理解,需要長時間的歷練才能很好的掌握DDD;

如何進行領域建模

按照實現領域驅動設計一書中描述的DDD步驟主要有4步:

  1. 根據業務需求劃分出初步的領域和限界上下文,以及上下文之間的關係;
  2. 進一步分析每個上下文內部,識別出哪些是實體,哪些是值物件;對實體、值物件進行關聯和聚合,劃分出聚合的範疇和聚合根;
  3. 為聚合根設計倉儲,並思考實體或值物件的建立方式;
  4. 在工程中實踐領域模型,並在實踐中檢驗模型的合理性,倒推模型中不足的地方並重構。

劃分領域和限界上下文

提煉問題域

軟體開發實際作用就是為客戶解決問題或者軟體升級變革,所以在領域中我們劃分了問題空間和解空間兩個緯度空間,用於描述客戶的問題(需求)和如何解決問題(解決方案)。

問題的來源:

  1. 客戶痛點需求:比如電腦藍屏、文件忘記儲存等。
  2. 行業變革:移動支付、網購等。

問題域劃分

首先我們需要對問題域進行拆分,拆分的理念就是由大化小,逐個擊破。拆分完畢問題空間,我們需要做到逐一擊破,為每一個問題形成一套落地解決方案。一個問題對應一個答案,解空間與問題空間基本上應該是一一對應的。

構建限界上下文

我們給出的解方案是一個個限界上下文,一個限界上下文負責應對其對應的問題進行解決,承載了該問題中的業務知識及規則。限界上下文是解決某一類問題的單一功能模組,例如訂單限界上下文,處理開單、通知訂單狀態等一切與訂單關聯的功能。

隨著問題的劃分,將領域拆分成了不同的子域,每個子域有其獨有的業務知識,而這些知識需要靠一門統一語言來描述。而不同限界上下文間的統一語言是相互獨立的。可能在不同的限界上下文有同樣的名詞,但同樣的名詞在不同的限界上下文中可能表示的不是同一事物,即使是同一事物可能其在不同上下文中的關注點也不一致。

限界上下文之間的對映關係:

  • 合作關係(Partnership):兩個上下文緊密合作的關係,一榮俱榮,一損俱損。
  • 共享核心(Shared Kernel):兩個上下文依賴部分共享的模型。
  • 客戶方-供應方開發(Customer-Supplier Development):上下文之間有組織的上下游依賴。
  • 遵奉者(Conformist):下游上下文只能盲目依賴上游上下文。
  • 防腐層(Anticorruption Layer):一個上下文通過一些適配和轉換與另一個上下文互動。
  • 開放主機服務(Open Host Service):定義一種協議來讓其他上下文來對本上下文進行訪問。
  • 釋出語言(Published Language):通常與OHS一起使用,用於定義開放主機的協議。
  • 大泥球(Big Ball of Mud):混雜在一起的上下文關係,邊界不清晰。
  • 另謀他路(SeparateWay):兩個完全沒有任何聯絡的上下文。

子域型別

核心域

核心域是一款軟體的所能獲得的競爭優勢的根本所在。所以在核心的建模和開發主要投入大量的時間和人力。

通用域

通用域是許多大型軟體系統都有的子域。通用域例如像電子郵件和簡訊傳送、OSS、MQ等。

支撐域

系統中的其他域被稱為支撐域。支撐域的主要作用就是有助於支撐核心域的業務。比如:使用者的註冊等。

分析模型(從領域建模角度分析需求)

DDD分析建模主要有兩種方法可以使用:用例分析法、四色建模法、事件風暴、領域驅動建模。

用例分析法

通過對需求進行分析構建出用例圖如下所示(舉例子不一定正確):

四色建模法

這裡列一下標準的四色建模法(https://www.infoq.cn/article/xh-four-color-modeling/),其實還是需要多聯絡才行。

時標型(Moment-Interval)物件:具有可追溯性的記錄運營或管理資料的時刻或時段物件,用紅色表示 -> 操作

PPT(Party/Place/Thing)物件:代表參與到流程中的參與方/地點/物,用綠色表示 -> 名詞

角色(Role)物件:在時標型物件與 PPT 物件(通常是參與方)之間參與的角色,用黃色表示 -> 操作角色

描述(Description)物件:對 PPT 物件的一種補充描述,用藍色表示 -> 表述備註

領域建模並不是沒有方法,而是選擇太多,有用例法、四色建模、領域驅動建模、事件風暴等,人有一個特點是喜歡找最簡單、最易用的方法,每個方法都有自己的特點,並沒有好壞優劣之分,只有適不適合。聽到一個方法就去嘗試,嘗試到一半就放棄,一種種方法嘗試,最終就會迷失在方法選擇上。

識別模型(主要構成)

 

構造模型(找到聚合根)

 

細化模型(反覆優化)

對於DDD而言領域建模完成後,我們就需要對照模型進行開發,在開發之間我們需要知道領域戰略設計中的幾種架構風格。

  1. 名詞做類、動詞為方法,生成漸層次模型
  2. 將漸層次模型歸納劃分,做更抽象的

DDD四層架構

DDD四層架構是一個鬆散分層架構(任意上層可以呼叫下層,但是下層不能呼叫上層),DDD四層架構的含義如下:

  1. 使用者介面層:使用者介面層,或者表現層,負責向使用者顯示解釋使用者命令類似於傳統三層架構中的Controller層);
  2. 應用層:定義軟體要完成的任務,並且指揮協調領域物件進行不同的操作。該層不包含業務領域知識。這層和傳統三層架構中不同點在於MVC中所有的業務邏輯都在Service層,但是DDD四層結構中將業務邏輯下層到領域層,應用層只負責定義方法,然後呼叫領域層進行處理。應用服務可以用於控制持久化事務和安全認證等。應用服務本身並不處理業務邏輯,但它確卻是領域模型的直接客戶。
  3. 領域層:領域層是系統的核心,負責表達業務概念,業務狀態資訊以及業務規則。即包含了該領域(問題域)所有複雜的業務知識抽象和規則定義。該層主要精力要放在領域物件分析上,可以從實體,值物件,聚合(聚合根),領域服務,領域事件,倉儲,工廠等方面入手。
  4. 基礎設施層:該層主要有兩方面內容,一是為領域模型提供持久化機制,當軟體需要持久化能力時候才需要進行規劃;一是對其他層提供通用的技術支援能力,如訊息通訊,通用工具,配置等的實現;

如下是DDD四層架構圖:

 

依賴倒置原則(DIP)

依賴倒置原則:我們要依賴不變或穩定的元素(類、模組或層),抽象不應該依賴於細節,細節應該依賴於抽象。依賴倒置原則可以改進分層架構,即底層服務應該依賴於高層元件所提供的介面。

 

採用了依賴注入方式後,其實可以發現事實上已經沒有分層概念了。無論高層還是底層,實際只依賴於抽象,整個分層好像被推平了,這就引入下一個架構六邊形架構。

六邊形架構

六邊形架構是Alistair Cockburn在2005年提出的,在這種架構中,不同的客戶通過“平等”的方式與系統互動。在《實現領域驅動設計》一書中,作者將六邊形架構應用到領域驅動設計的實現,六邊形的內部代表了application和domain層。外部代表應用的驅動邏輯、基礎設施或其他應用。內部通過埠和外部系統通訊,埠代表了一定協議,以API呈現。個人理解其實就是變成內層、適配層&外部資源,內層是應用層和領域層;適配層是之前的介面層和基礎設施層;外部資源就是對外介面、快取、MQ、資料庫等等。六邊形架構圖如下所示:

 

下面是對六邊型架構的一個落地實踐示例:

四層架構、DIP、六邊形架構可以參考:

https://www.jianshu.com/p/c405aa19a049

http://it.hzqiuxm.com/ddd%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E6%88%98%E7%95%A5%E7%AF%87%EF%BC%884%EF%BC%89/#DDD-2

CQRS

在DDD設計思路中,DDD 通過領域物件之間的互動實現業務邏輯與流程,並通過分層的方式將業務邏輯剝離出來,單獨進行維護,從而控制業務本身的複雜度。但是作為一個業務系統,查詢的相關功能也是不可或缺的。在實現各式各樣的查詢功能時,往往會發現很難用領域模型來實現。假設在使用者需要一個訂單相關資訊的查詢功能,展現的是查詢結果的列表。列表中的資料來自於訂單、商品、品類、送貨地址等多個領域物件中的某幾個欄位。這樣的場景如果還是通過領域物件來封裝就顯的很麻煩,其次與領域知識也沒有太緊密的關係。所以命令查詢職責分離的設計架構(CQRS)是可以很好的解決上述的問題。

CQRS將系統分為兩類操作:命令(command)、查詢(Query)。命令則是對會引起資料發生變化操作的總稱,即我們常說的新增,更新,刪除這些操作,都是命令。而查詢則和字面意思一樣,即不會對資料產生變化的操作,只是按照某些條件查詢資料。CQRS 的核心思想是將這兩類不同的操作進行分離,然後在兩個獨立的服務中實現。這種獨立服務可以是兩個不同的應用或者同一個應用中不同的包進行隔離。同時理論上命令和查詢的資料來源也是不同的(實現讀寫分離)。

當然讀寫分離模式,肯定會遇到事務的問題,事務問題主要的包含兩種情況:

  1. 當 command 端完成資料更新後,需要通過事件的形式通知 query 端系統,這就存在著一定的時間差,如果你的業務對於資料完整的實時性非常高,那麼可能 CQRS 不一定適合你。
  2. 其次一個 command 觸發的事件在 query 端可能需要更新數個資料模型,而這也是有可能失敗的。一旦更新失敗那麼資料就會長時間的處於不一致狀態,需要外部的介入。

所以從事務的角度來看 CQRS,你需要面對的是問題從根本來說是個最終一致性的問題。對於團隊處理這種最終一致性問題需要有對應的方案。

CQRS架構如下所示:

 

領域物件

瞭解了戰略設計後,我們主要做具體的實施,具體實施時我們需要了解實體、值物件和聚合這幾個領域物件。

實體&值物件

我們先看一下他們的定義:

實體:許多物件不是由它們的屬性來定義,而是通過一系列的連續性(continuity)和標識(identity)來從根本上定義的。只要一個物件在生命週期中能夠保持連續性,並且獨立於它的屬性(即使這些屬性對系統使用者非常重要),那它就是一個實體。

值物件:當你只關心某個物件的屬性時,該物件便可作為一個值物件。為其新增有意義的屬性,並賦予它相應的行為。我們需要將值物件看成不變物件,不要給它任何身份標識,還應該儘量避免像實體物件一樣的複雜性。

對於實體Entity,實體核心是用唯一的識別符號來定義,而不是通過屬性來定義。即即使屬性完全相同也可能是兩個不同的物件。同時實體本身有狀態的,實體又演進的生命週期,實體本身會體現出相關的業務行為,業務行為會實體屬性或狀態造成影響和改變。

如果從值物件本身無狀態,不可變,並且不分配具體的標識層面來看。那麼值物件可以僅僅理解為實際的Entity物件的一個屬性結合而已。該值物件附屬在一個實際的實體物件上面。值物件本身不存在一個獨立的生命週期,也一般不會產生獨立的行為。

對於實體和值物件詳細說明可以參考:https://www.jianshu.com/p/da51d16dbdc4

聚合&聚合根

聚合是業務和邏輯緊密關聯的實體和值物件組合而成,聚合是資料修改和持久化的基本單元,一個聚合對應一個資料的持久化;同時將選擇一個實體作為每個聚合的根,並僅允許外部物件持有對聚合根的引用。作為一個整體來定義聚合的屬性和不變數,並把其執行責任賦予聚合根或指定的框架機制。

https://www.cnblogs.com/laozhang-is-phi/p/9916785.html

最小化整合

在所有的DDD專案中,通常存在多個限界上下文,這意味著我們需要找到合適的方法對這些上下文進行整合。當模型概念從上游上下文流入下游上下文中時, 儘量使用值物件來表示這些概念。這樣的好處是可以達到最小化整合,即可以最小化下游模型中用於管理職責的屬性數目。使用不變的值物件使得我們做更少的職責假設。

貧血模型&充血模型

貧血模型:指使用的領域物件中只有setter和getter方法(POJO),所有的業務邏輯都不包含在領域物件中而是放在業務邏輯層。貧血模型中領域物件只有屬性沒有豐富的操作,使用時需要單獨的Dao層配合使用,風格比較程式導向。

充血模型:將大多數業務邏輯和持久化放在領域物件中,業務邏輯只是完成對業務邏輯的封裝、事務和許可權等的處理。充血模型更加傾向於物件導向的設計風格(可以參考:https://www.jianshu.com/p/fae3da337ebc)。

模組&領域服務&工廠

模組(Module)是DDD中明確提到的一種控制限界上下文的手段,在我們的工程中,一般儘量用一個模組來表示一個領域的限界上下文。一般的工程中包的組織方式為{com.公司名.組織架構.業務.上下文.*},這樣的組織結構能夠明確的將一個上下文限定在包的內部。
領域服務:領域中的服務表示一個無狀態的操作,它用於實現特定於某個領域的任務。
工廠:在DDD中有可能存在複雜物件的建立,如果使用物件本身進行建立有可能會破壞單一責任原則,所以需要工廠進行物件建立。

相關文章