理解領域驅動設計

前言

什麼是領域,我習慣描述的是製藥領域、環境領域、建築領域、金融領域等,而在領域內,各類業務規則、業務知識盛行,如何有效的把控規則的變化,應對複雜知識,有一個很關鍵的四字詞語,分而治之。分治法在不少場景下體現了其強大的做用力。領域自己很大,那就拆分,獲得更小的領域,也即子域,如同遞歸調用通常,將一個複雜問題拆分單獨求解,而最終將解彙總獲得複雜問題解。html

怎麼拆,拆成怎麼樣合適,依據什麼拆,這些在領域驅動設計中有了一套答案,雖然領域驅動設計不是銀彈,但能夠說的上是一套極好的系統方法論或稱爲架構設計的方法論。前端

領域驅動設計常以戰略設計與戰術設計來將整個領域展示的淋漓盡致,其做用範圍既面向業務也面向技術。從戰略角度(我的更喜歡稱其爲上帝視角)去規劃系統、劃分領域。而從戰術角度則從技術層面來指導咱們該如何去設計。git

戰略設計

戰略設計主要從高層俯視(上帝視角)咱們的軟件系統,就如同玩即時戰略遊戲般,能夠一覽地圖全貌,以此來決定咱們是要進攻仍是防守哪一個方向,一樣,在軟件中咱們也能夠以此來劃分領域,肯定權重方向。數據庫

統一語言

提煉領域知識,怎麼個提煉法,千萬條羅馬路,各有各的看家本領。像事件風暴方法,用例分析方法,用戶故事,甚至是開大會,各類討論會等,最終目的都是提煉出領域知識,而提煉過程當中,達成描述上的一致性,包括系統目標、系統範圍及系統所具備的功能。c#

圖片

這不是領域驅動設計所獨有的,但倒是軟件開發中所必須的,爲領域專家、業務分析人員、編碼人員和測試人員等團隊全部成員交流時構建統一頻道。後端

圖片

領域/子域

圖片

領域拆分

對於領域這個概念,習慣性會想到製藥領域、環境領域、金融領域等這些概念,而領域自己所描述的是範圍,是如同現實世界般的複雜,無邊際。藉助分治法,將問題逐級細分來下降業務和技術複雜度,將這複雜的世界劃分出清晰的邊界來,反過來控制着劃分後不那麼複雜的世界,也既領域拆分出細化後的子領域。架構

圖片

子域劃分

在實際解決問題時,咱們也習慣將問題拆分,而怎麼拆,基於什麼原則拆,可能會依據相關性,權重,甚至分類原則等,對於系統而言,會從架構方面考慮,基礎設施考慮等,在領域驅動設計中,更偏向基於業務拆分,下降業務複雜度,也分離技術實現的複雜度,依照業務拆分後的子領域,自己存在權重上的差別,依照重要性和功能劃分爲三類,投資佔比也就有所不一樣。app

  • 核心域:其所體現的是核心服務,是表明着產品的核心競爭力。
  • 支撐域:其所體現的是支撐服務,沒它不行,但又達不到核心的價值,圍繞着產品內部所須要,但又不能單獨變動爲第三方服務,即它不是一個通用的服務。
  • 通用域:其所體現的中間件服務或第三方服務。自己能夠經過現有的解決方案集成來完成的服務。

圖片

限界上下文

深刻到一個子域中,又是一片小天地,在這天地中,卻又仍是存在着因語義與語境上的差別,讓一些概念在這子域中顯得額外尷尬。在一個領域 / 子域中,咱們會建立一個概念上的領域邊界,在這個邊界中,任何領域對象都只表示特定於該邊界內部的確切含義。這樣邊界便稱爲限界上下文。dom

其本質上是限界+上下文,引用到張逸老師的一句話async

上下文(Context)實際上是動態的業務流程被邊界(Bounded)靜態切分的產物

對於子域與上下文間的關係,看到不少書籍或是文章中所描述的都不同,這塊的爭論也沒有一個最終答案,我的更傾向於子域中劃分上下文,從拆分角度來說,這樣理解更加簡單。

圖片

上下文識別

對於上下文的識別,沒有可遵循的標準可走,從不一樣的角度切入將會識別到不一樣的上下文,可從張逸老師的領域驅動設計實踐中窺之一二,以業務複雜度、管理複雜度和技術複雜度出發,面對這三個角度去依次分析,從業務視角、工做視角、應用視角去識別,進而識別出準確的上下文,經過不斷的分析斟酌考慮,逐漸識別出符合當前預期的上下文,如在實際操做環節發覺當前上下文的設計顯得不那麼合理,還可再進行變更、拆分上下文。

圖片

但需注意的一個是,咱們識別上下文的目的是什麼,是爲了控制上下文,準確的說是爲了控制上下文的邊界、大小,是爲了保住咱們所守護的上下文不會因過分成長變大而奔潰,亦或因上下文過分縮減而失去價值,保證上下文內一切的穩定,上下文與上下文間交互的可用性,也或者是當咱們退出上下文時,交付出來的上下文是很是可觀的,而不是一個爛攤子。

上下文映射

規劃了這麼多限界上下文,該如何穿針引線將這些上下文串起來即是一個問題了,用例場景的完整實現每每是由多個上下文的協做完成的,怎麼去組織這些上下文,領域驅動設計提到的幾種方式及軟件工程中經常使用模式。

  • 合做關係:一榮俱榮,一損俱損。
  • 共享內核:上下文間共享領域實體。
  • 客戶方-供應方:下游客戶依賴於上游供應方。
  • 遵奉者:下游客戶順應上游供應方。
  • 各行其道:沒有關係的關係,相互隔離。
  • 防腐層:在下游上下文與上游間增長一道屏障,以此來隔絕與上游的直接交互保護下游。
  • 開放主機服務:在上游與下游上下文間增長一道協議,以此來規範下游對上游的集成。
  • 已發佈語言:發佈方上下文發佈一份包含豐富文檔的信息交換語言,消費方上下文翻譯並使用。

這些模式其本質是爲了協做,爲了知足用例場景下對多個限界上下文的調用,經過上下文映射圖,能夠清楚知曉運行邏輯。爲了實現上下文映射,簡單講就是如何將兩個上下文連貫起來,常藉助的方式是諸如 RPC、HTTP、消息隊列等,依照上下文間映射類型,挑選一件趁手的工具。

圖片

分層架構

咱們一般喜歡對各類事情概括總結,如文章的井井有條,如建築結構高低有序、疏密有致,給人一種各處所關注的信息視角不一樣,而組合起來顯得如此美妙。軟件中一樣運用着分層來隔離關注點,以此來隔離每層的演進速率。

當咱們考慮限界上下文時,不只須要去考慮其內部的領域設計,還得從其應用邊界自己考慮,限界上下文是屬於架構設計層次,主要針對的是後端架構層次的垂直切分,按照經典 DDD 的分層結構來看,共分爲以下四層:

圖片

  • User Interface 爲用戶界面層,向用戶展現信息和傳入用戶命令。這裏指的用戶不僅僅只使用用戶界面的人,也多是外部系統,諸如用例中的參與者。
  • Application 爲應用層,用來協調應用的活動,不包含業務邏輯,經過編排領域模型,包括領域對象及領域服務,使它們互相協做。不保留業務對象的狀態,但它保有應用任務的進度狀態。
  • Domain 爲領域層,負責表達業務概念,業務狀態信息以及業務規則。儘管保存業務狀態的技術細節是由基礎設施層實現的,可是反映業務狀況的狀態是由本層控制而且使用的。領域層是業務軟件的核心,領域模型位於這一層。
  • Infrastructure 爲基礎實施層,提供公共的基礎設施組件,如持久化機制、消息管道的讀取寫入、文件服務的讀取寫入、調用郵件服務、對外部系統的調用等等。

值得注意的是,給定的分層方式僅僅是邏輯上的分層,而對於實際的物理分層,卻又有所不一樣,但遵照一個前提爲好,即限界上下文的邊界高於分層的邊界。諸如以下兩種開發中常見的代碼組織方式,均可見到。一種是基於技術分層,而另外一種更偏向基於業務分層。

方式一

- application
  - productcontext
  - ordercontext
  - ...
- domain
  - productcontext
  - ordercontext
  - ...
- infrastructure
  - productcontext
  - ordercontext
  - ...

方式二

- productcontext
  - application
  - domain
  - infrastructure
- ordercontext
  - application
  - domain
  - infrastructure

具體採用哪一種方式,並無強制要求,不管代碼組織結構是否表達了層的概念,都須要充分理解分層的意義,並使得整個代碼結構在架構上要吻合分層架構的理念。

戰術設計

相比於戰略設計的怎麼規劃,戰術設計更側重於怎麼執行,詳細的設計和編碼。

圖片

聚合

在認識聚合前,咱們得對類再次回顧,類是做爲咱們開發中的最小單元,一切以類構建,而在上下文的視角中,聚合成了最小概念,包裝了一組高度相關的對象,上下文內以聚合爲最小單元,以此來保證聚合邊界。又將分而治之的思想融入到了限界上下文的內部。

聚合自己是由一個或多個實體及值對象組成,其中一個實體做爲聚合根。管理着內部關聯的實體與值對象,對外表明着聚合,外部來訪者僅可經過聚合根進行訪問。

圖片

對於聚合圖的畫法,或許因人而異,我更加傾向於用矩形表明實體,橢圓表明值對象,用 UML 類圖中的組合-聚合箭頭來表示其雙方間的關係。

須要注意的是,此處的聚合不要與 UML 類圖中的聚合等同起來,二者含義並不相同。

實體

對於實體來說,這個概念對於咱們並不陌生,擁有者惟一的身份標識符,內含屬性做爲該實體的靜態特徵,做爲聚合所擁有的領域知識,擁有着與自身相關的領域行爲。

值對象

對於值對象,我傾向於將它理解爲,基礎類型之延伸,既能封裝基礎類型,又能約束內部屬性間關係,還能擁有着自身的領域行爲,而與實體的區別是,沒有惟一身份標識,儘管帶來了持久化的一些問題,但仍是存在解決方案。以 DateTime 理解值對象最好不過了,DateTime 內部的自身約束保證了,每一次變更的 DateTime 都是最新的,當咱們想在 2 月 28 日加 1,這便要依靠 DateTime 中的行爲去約束內部的屬性。

聚合劃分

經統一語言與業務分析階段,藉助一系列如事件風暴、用例分析法、名次動詞法、四色建模法等活動後,得到了一系列相關聯的對象。或可造成一張龐大的對象關聯圖。

圖片

如不考慮聚合的劃分,咱們依照以往的思路即是建立一大堆表,運用三範式或是依靠程序去保證數據的一致性不運用主外鍵。而後瘋狂擼碼,CRUD 好不快活。

而隨着業務的逐漸擴張,這當初的想法已有點吃力了,如同樹苗逐漸成長,枝葉也逐漸增多。藉助枝幹咱們能夠分清葉子的歸屬,而對象網中呢,變得錯綜複雜了,也就隱約有了大泥球的徵兆。

藉助劃分聚合的一些方法,將其規整化。將原有複雜的對象圖拆分紅可控制的小型對象圖。

  • 保持單一導航方向,解除雙向依賴,保持依賴簡單。
  • 保持聚合設計的小巧
  • 聚合內的業務規則一致性
  • 經過聚合標識符引用其餘聚合
  • 聚合與協做聚合間因業務場景、進程邊界等因素影響,可依照場景使用強一致性或是最終一致性。

如上的對象圖依照關係的強弱,關係的主與次進行了聚合劃分,或許得出的部分聚合存在不合理處,可再調整其邊界。

圖片

聚合協做

聚合與協做聚合之間依照聚合根實體的惟一標識符進行關聯,而不是經過依靠協做聚合的引用實例來完成。保持這個原則有助於保持聚合之間的邊界並避免加載沒必要要的對象。如咱們常習慣上將關聯的集合對象寫入到類中,而後在倉儲使用時,經過 EF 加載導航屬性,以此方便直接加載關聯聚合數據。

//一個聚合內建議用
public class Order : AggregateRoot
{
    public virtual ICollection<OrderItem> OrdrItems { get; set; }
    //...
}
_orderRepository.Include(e=>e.OrderItems).FirstOrDefault();

如 Order 和 OrderItem,當咱們考慮將其做爲一個聚合時,這麼使用,是能夠的,可是不能說跨聚合也這麼用着,如 Enterprise 和 Order,劃分時咱們更加傾向於劃分爲兩個聚合,遵循保持聚合原則中,引用聚合根的 Id 這一原則,這將改善聚合的邊界使其更加清晰,控制更加穩當。

//多聚合間不建議這麼用
public class Order : AggregateRoot
{
    //遵循聚合原則引用 Enterprice 聚合根 Id,而不是實例
    public int EnterpriceId {get; set;}
    //public virtual Enterprice Enterprice { get; set; }
    //...
}

考慮到多聚合的協做,便要了解下聚合的首要原則,即在一次事務中,只能更改一個聚合的狀態,所以當涉及到多個聚合協做時,如建立訂單完畢,須要往庫存中某一商品數量減小時,訂單自己通常會有商品聚合的標識,藉助這個標識,經過領域事件或是集成事件方式,事件接收方將相關聯的庫存聚合調用起來,以此達到多個聚合間的協做。
又或者考慮到,須要調用商品的信息以使得當前訂單中商品信息更加豐富,可經過防腐層調用商品所在上下文遠程服務或是應用服務,最終本質上是調用商品聚合中的信息豐富到訂單中,也使得多個聚合完成協做。

圖片

應用服務

做爲限界上下文對外的門戶,也便是外觀模式的體現。經過用例分析識別出來的用例在此處一一對應存在着,對外提供統一接口,以此知足完整用例場景所需的功能。在應用服務內部,經過編排領域模型對象來完成用例的功能,自身並不包含領域邏輯,但包含着應用邏輯。

圖片

可借鑑整潔架構的經典圖例來看應用層自己的職責所在,Use Case(用例層)-Application Business Rules,雖然是依靠着領域模型對象才完成的(具體是編排領域模型對象所具備的領域行爲),卻也說明了應用服務承擔着的是用例的職責。

須要注意的是,應用服務的職責不只限於編排領域模型對象,還須要控制着橫切關注點,如驗證、日誌、事物等的管理。

[UnitOfWork]
[Authorize(PermissionNames.PartType_Create)]
public async Task CreatePartType(CreatePartTypeDto input)
{
    await _validatingPartTypeManager.CheckUniqueName(input.Name, input.Category);
    var partType= PartType.Create(input.Name, input.Description)
        .SetCategory(input.Category)
        .SetFactory(input.FactoryName);
    await _partTypeRepository.InsertAsync(partType);
    await _appNotifier.NewPartTypeAsync();
}

如上,事務、認證、請求參數校驗(Dto 內),協調領域模型對象和基礎設施服務,這是應用服務的職責,固然也不只限於這些職責。

領域服務

當咱們考慮領域邏輯時,首先想到的應該是實體與值對象中具備的領域邏輯,而有些場景下,實體與值對象沒法承載這些領域行爲,如對多個領域對象做爲輸入,進行計算併產出一個值對象;又或是須要將操做成集合化的聚合,如在 Supplier 下須要將全部 Order 中的單價彙總,而自己 Supplier 和 Order 是爲兩個聚合,若考慮藉助 Order 去完成該業務操做,不太穩當,在此場景下,可經過領域服務來承載着這些領域行爲。

領域服務存在以下特徵:

  • 執行一個顯著的業務操做過程
  • 對領域對象進行轉換
  • 須要使用多個聚合內的實體和值對象編排業務邏輯
  • 領域行爲須要訪問外部資源

雖然說領域服務可以承載領域邏輯,卻不能說將全部的領域邏輯都往裏塞,如此,致使領域對象貧血。只有當實體與值對象承載不住或是自己並不屬於實體或值對象的職責內時,才考慮領域服務來承載,領域服務是一種妥協的結果,並非說領域服務越多越好。

值對象(Value Object)→ 實體(Entity)→ 領域服務(Domain Service)

以下場景,建立 Invoice,存在幾條業務規則,相應 Order 的狀態需已完成,而且對應的 Supplier 提供財月信息,這就須要多個聚合的協做,在領域服務編排這些領域對象模型及經過調用外部服務網關,完成業務邏輯。

// InvoiceManager
public async Task ValidCheck(string orderId, string supplierId)
{
    var order = await _orderService.GetAsync(orderId);
    if(!order.IsCompleted())
    {
      throw new UserFriendlyException("Order status is not completed");
    }
    
    var supplier = await _supplierService.GetAsync(supplierId);
    if(!supplier.IsCompleted())
    {
      throw new UserFriendlyException("Order status is not completed");
    }
}
public async Task SetFinanceMonth(Invoice invoice, string supplierId)
{
    var supplierFinanceMonth = await _supplierService.GetFinanceMonthAsync(supplierId, Current.Date);
    
    if(supplierFinanceMonth == null)
    {
      throw new UserFriendlyException("Supplier not provider finance month");
    }
    
    invoice.SetFinanceMonth(supplierFinanceMonth.StartDate, supplierFinanceMonth.EndDate);
}

在應用服務中,經過調用聚合及領域服務,完成這一建立 Invoice 的用例。

[UnitOfWork]
[Authorize(PermissionNames.Invoice_Create)]
public async Task CreateInvoice(CreateInvoiceDto input)
{
    await _invoiceManager.ValidCheck(input.orderId, input.SupplierId);
    var invoice = Invoice.Create(input.Name, input.Description)
        .SetOrder(input.OrderId);
    await _invoiceManager.SetFinanceMonth(invoice, input.SupplierId);
    await _invoiceRepository.InsertAsync(invoice);
    await _appNotifier.NewInvoiceAsync();
}

藉助領域服務,以此來完成多聚合間的協做,經過應用服務編排領域模型對象,完成一個業務用例。

領域事件

在軟件開發中,事件早已被咱們所熟悉,一個按鈕按下,產生中斷事件,一個回車,前端頁面有偵聽事件,在事件風暴建模活動中,事件也是做爲領域建模的突破口,事件的重要性不言而喻。其本質是發生的事實到引起了相關事情,在這其中的傳遞的信息即是事件的內容。就如同貓叫了,引起着老鼠跑了,主人醒了,其中的事件即是貓叫了,而該事件是貓執行叫的動做後的結果。

在領域驅動設計中,最開始的版本中並無領域事件的概念,在 DDD 社區對領域驅動設計的內容不斷的充實中,引入了領域事件。領域事件的命名遵循英語中的「名詞 + 動詞過去分詞」格式,如,提交訂單後發佈的 OrderCreated 事件,訂單完成後 OrderCompleted 事件,用以表示咱們建模的領域中發生過的一件事情,也符合着事件自己是具備時間特徵。

圖片

(EShopOnContainers 中一個例子)

對於領域事件自己,依據各層的使用方式及面對的目標不一樣,劃分出兩種事件類型,領域事件與應用事件(或集成事件),應用事件側重於應用層的使用,而領域事件沿用原領域事件的稱呼,更偏向於領域層。而又應側重點不一樣,又有着不一樣的使用方式,如領域事件更多的是從領域模型中發佈,其最終接收者爲當前聚合所在限界上下文,而應用事件更爲廣闊,從應用層發佈,其接收者爲當前上下文或是其餘上下文。

基於限界上下文間採用的部署方式不一樣,也存在着不一樣的通訊方式,如整個應用程序爲單體,則全部上下文在同一個進程內,則上下文間事件交互時所採用的能夠是進程內的事件總線,或是進程間使用的消息隊列,而當在進程間時,就不得不使用進程間的消息隊列了。

因爲 DDD 中遵循一個用例對應一個事務,在一個事務中更新一個聚合,所以對於實際場景中須要變動多個聚合下,咱們常經過編排方式調用其餘聚合的服務,這不可避免的加劇了對其餘服務的依賴,藉助領域事件,則能夠很方便的下降這種耦合,同時對於多個聚合的變動操做,由單個聚合的事務變成了多個聚合的事務,又依照實際影響的聚合狀況,有着不一樣的處理方式,如多個協做的聚合爲同一上下文內時,可經過強一致性去保證數據一致性,而處於多個限界上下文間的聚合時,則可依照最終一致性保證數據的一致性。

領域事件主要用途有:

  • 從事件角度豐富了領域模型
  • 保證聚合間的數據一致性
  • 實現事件事件溯源和 CQRS 等
  • 限界上下文間集成(發佈訂閱模式)

資源庫

在剛接觸資源庫(Repository)時,第一反應即是這就是個 DAO 層,訪問數據庫,而後吧啦吧啦,可是,當接觸的越久,愈加認識到第一反應是錯的,資源庫更多的是對資源的管理,而不只僅是數據庫中的數據,數據庫能夠做爲資源的一部分,但不是所有,咱們習慣將對外部系統的調用稱爲外部資源的獲取,這也是將外部系統做爲資源的一部分。

對於聚合來說,資源庫的做用是負責將聚合持久化到數據庫的(一般是持久化到數據庫),而且因爲聚合根負責維持聚合的生命週期,也就使得應考慮僅聚合根才應該擁有資源庫,這也是與 DAO 層不一樣的地方。

在分層設計時,考慮將資源庫的抽象劃分到領域層,屬於領域模型對象的一部分,如同設計防腐層的抽象網關般,資源庫的抽象做爲特殊的網關,當在應用層或是領域層中操做資源庫抽象時,將資源庫做爲管理聚合狀態的工具,能夠忽視基礎設施層中對資源庫的具體實現。而在考慮基礎設施層中具體實現時,可根據須要選擇適合的工具,以此來管理和操做資源。

圖片

工廠

聚合從 0 到 1 的過程,能夠經過多種途徑建立,通常來說,咱們開發中常直接實例化或是反射實例化,而對於聚合來說,整個聚合是一個總體,命運共同體,而且由聚合根掌握聚合的生命週期。一般,咱們能夠藉助幾種方式來建立聚合,組裝聚合,在建立過程當中封裝業務邏輯。

  • 聚合自身擔任工廠,在聚合根中實現 Factory 方法
  • 獨立的 Factory 類,用於有必定複雜度的建立過程,或者建立邏輯不適合放在聚合根上
  • 藉助其餘聚合來建立,其餘聚合擔任工廠角色
  • 藉助構建者模式靈活組裝聚合

聚合根的建立有多種方式,依據聚合內掌握知識的多少與建立邏輯的須要可靈活選擇。

//...
var partType= PartType.Create(input.Name, input.Description)
    .SetCategory(input.Category)
    .SetFactory(input.FactoryName);

如藉助構建者模式,經過拆分許多小的方法,將過多的參數拆分,以此避免一個建立方法參數中滿屏都是參數的狀況,須要考慮吧拆分的方法須要知足業務一致性,如內部的一些屬性間有約束條件下,須要劃分到一個方法中,以維持一致性或不變性。

學無止境

從2004年領域驅動設計到如今已經有17年時間了,而且在其中還有諸如六邊形架構,洋蔥架構,整潔架構等的出現,考慮的側重點不一樣,衍生着大量的新概念,也不斷地完善着領域驅動設計的思想。在學習與理解領域驅動設計中,總會有新的東西改變咱們以往的思想,見到的越多,愈加覺認識的越少,這或許也是學起來有點阻力的緣由吧。

圖片

參考

  1. 《實現領域驅動設計》- Vaughn Verno
  2. 《領域驅動設計實踐》- 張逸
  3. 《軟件架構編年史》- herbertograca
  4. 領域驅動設計實現之路 - 滕雲
  5. 領域驅動設計編碼實踐 - 滕雲
  6. Package by component and architecturally-aligned testing - Simon

2021-01-18,望技術有成後能回來看見本身的腳步

相關文章
相關標籤/搜索