主頁 > 軟體設計 > 何時使用領域驅動設計

何時使用領域驅動設計

2021-09-01 09:11:05 軟體設計

何時使用領域驅動設計?其實當你的應用程式架構設計是面向業務的時候,你已經開始使用領域驅動設計了,領域驅動設計既不是架構風格(Architecture Style),也不是架構模式(Architecture Pattern),它也不是一種軟體開發方法論,所以,是否應該使用領域驅動設計,以及什么時候使用領域驅動設計,這個問題本身就比較復雜(或者說這并不是一個好問題),或許,更精確的提問方式應該是:“我應該選擇什么樣的架構風格來構建我的系統?”,現在我們先不急著回答這個問題,還是回到領域驅動設計的話題上,來回顧一下領域驅動設計里的基本概念,

領域驅動設計

很多人都了解測驗驅動開發(TDD)、功能驅動開發(FDD)、API驅動開發(ADD)和行為驅動開發(BDD),那么什么又是領域驅動設計(DDD)呢?DDD的第三個D為什么是“設計”而不是“開發”呢?領域驅動設計最開始提出來的目的是為了簡化業務人員與開發團隊之間的溝通,以保證開發出來的軟體產品不僅能夠很好地解決業務領域問題并滿足客戶的需求,而且還能夠簡化或解決傳統軟體開發程序中遇到的各種問題(比如需求變更、橫向或縱向擴展性差等等),因此,通用語言(ubiquitous language)就是領域驅動設計中最重要最核心的概念:它能夠確保代碼的組織方式能夠直接反映業務模型和業務邏輯,并且在整個業務系統中,對于同一個業務概念使用相同的代碼表述(比如銀行系統中的Account物件),從通用語言的定義出發,領域驅動設計對于業務領域建模提供了一些指引,具體表現為引入了物體(Entity)、值物件(Value Object)、服務(Service)、聚合(Aggregate)、聚合根(Aggregate Root)、工廠(Factory)和倉儲(Repository),這里我就不打算深入討論這些概念了,就簡單回顧一下吧,

領域建模三劍客:物體、值物件和服務

在進行領域建模時,領域驅動設計引入了三個概念:物體、值物件和服務,物體和值物件都能夠反映真實世界中的一個業務概念,兩者的區別是,物體通過特定的識別符號(ID)來確定一個個體,而值物件則是通過物件本身各個欄位的值來確定一個個體,例如,某班的學生資訊,學生(Student)就是一個物體,在進行領域建模的時候,一般會使用學號作為學生的ID,因為沒有任何一個或者一組學生身上的屬性能夠唯一確定一個學生:姓名不行,出生日期不行,身份證號也不行(撇開有可能重號不說,用身份證號來標識學生會帶來資訊泄露問題);再比如學生的聯系地址(Address)則是一個值物件,因為系統可以通過國家、省份、城市、街道和門牌號這些值的組合來唯一確定一個地址, 為物體設計一個合理的識別符號(ID)策略,通常情況下并不是一件簡單的事情:識別符號需要具備全域唯一、生成高效、存盤友好、意義鮮明這些基本特質,所以,Guid并不是一個很好的選擇:它全域唯一、生成高效,然而并非存盤/索引友好,而且是一串字符加數字和橫杠,不代表任何意義,很多應用系統會有專門的服務來產生滿足條件的識別符號,比如銷售系統很有可能會有單獨的分布式服務來生成一個由訂單日期、客戶ID、訂單流水號以及校驗碼組成的一長串字串來用作訂單編號,總而言之,為領域模型中的物體物件實作一個識別符號的生成機制可以有很多種方法,這里也不進一步展開了,但是你會發現,領域驅動設計在這里只告訴你,物體需要一個ID,如何實作?這不是領域驅動設計的討論范疇,因此也就回答了上面“第三個D為什么是‘設計’而不是‘開發’”的問題, 由于領域模型中的物件都是對業務概念的真實反映,所以,物件不僅會有狀態,而且還會有行為,應該盡可能地將業務行為設計到合理的領域模型物件上,而不是將領域模型物件全部都設計成POCO/POJO,然后將所有業務行為都塞到Transaction Script里,例如:學生會有寫作業的行為,因此,doHomeWork(Homework homework)方法就應該設計在“學生”物體上,然而,有些情況下,某些業務行為很難歸結到某個物體或者值物件上,一個經典的例子就是銀行業務里的轉賬(transfer)方法,它并不是某個銀行賬戶(Account)的行為,可能是銀行的行為,也可能是用戶的行為,在這種情況下,領域驅動設計引入了服務的概念:在服務上定義從領域角度無法歸結到任何一種模型物件上的行為,由此可見,服務是領域建模中的一部分,也是領域模型的重要組成部分,

生命周期雙子星:工廠和倉儲

有了領域物件,自然就需要管理物件的生命周期,在介紹工廠和倉儲之前,先看一下與領域物件相關的兩個抽象概念:聚合與聚合根,聚合是能夠表達一個完整的領域概念(或者說業務概念)的物體和值物件的組合,如果用UML類圖來表示聚合,應該選擇使用組合模式,不難理解,聚合里的所有物體和值物件都有相同的生命周期,它們被同時創建,也被同時銷毀,對于每一個聚合,必定有一個物體其本身就代表了整個聚合的業務意義,比如“銷售訂單”聚合可以由“銷售訂單”物體、“銷售訂單明細”物體以及“聯系地址”值物件組成,而其中的“銷售訂單”物體就代表了整個聚合的業務意義,像這樣的物體,我們稱之為聚合根,當然,有些聚合僅包含一個物體,而這個聚合的聚合根就是這個物體本身,所有與生命周期相關的操作都應該發生在聚合根上, 在領域驅動設計中,工廠負責創建聚合,而倉儲負責聚合的持久化、激活以及銷毀,這些操作都是應用在聚合根上,同樣,領域驅動設計并沒有討論工廠和倉儲應該如何實作,然而基于它們本身的特點,在實際中我們更多地會選擇一些創建型模式來實作工廠,而選擇一些資料持久化機制(比如資料庫)來實作倉儲,就倉儲的實作而言,我們基本上會結合底層的資料存盤技術選型來決定倉儲的設計,甚至會將其抽象成倉儲設計模式,在不同的架構風格下,倉儲的職責也會有所不同:傳統分層架構下,倉儲是有查詢職責的,因為它需要基于聚合根來重建整個聚合,然而,在基于事件的CQRS架構中,倉儲的查詢職責變得非常薄弱,這是由于讀寫分離造成的, 以上基本上對領域驅動設計的基礎性內容進行了回顧,如果你的專案正在,或者將要遵循上面的這些概念和指引進行業務分析與領域建模,或者在進行需求分析的時候,你的團隊也在不停地考慮如何在軟體中設計你所要面對的這些業務物件,并且在不停地梳理相關的領域知識,那么恭喜你,你已經步入了領域驅動設計的正軌,當然,在領域模型建立的程序中,你會發現很多問題,比如你會發現,銀行賬戶與互聯網登錄賬戶都叫“賬戶”,但它們卻是完全不同的東西;你甚至會發現,雖然都是“銀行賬戶”,但在不同的場景下它所表述的意義完全不同(例如用于支付的支付賬戶與用戶的定期賬戶是兩碼事),對于這些問題,領域驅動設計也提出了相應的解決方案,比如引入“界定背景關系(Bounded Context)”的概念,而這一概念也剛好契合了目前最流行的軟體架構風格:微服務架構風格,下文再深入討論, 接下來你可以考慮本文剛開始的問題:我應該選擇什么樣的架構風格來構建我的系統,

軟體系統架構風格

通常情況下,我們會選擇一種軟體架構風格來實作軟體系統,而在開發的程序中,我們還會應用很多開發模式并且引入一些開發方法論,比如在模型持久化部分,我們會選擇倉儲模式,而在構建領域物件模型時,又有可能用到訪問者模式,我們還會選擇使用敏捷開發方法論來指導我們的日常開發任務等等,由此可見,軟體系統架構風格并非是一種模式,簡單地說,架構風格決定了系統將由哪些組件組成,以及這些組件之間的關系如何,而架構模式則表述了如何實作這些組件以及處理它們之間的關系, 在《面向模式的軟體體系結構(卷一):模式系統》一書中,將軟體設計模式分為三種:體系結構模式設計模式以及慣用法,體系結構模式也就是架構模式,常見的有黑板模式、分層模式、MVC、發布者/訂閱者、Proactor/Reactor、命令查詢職責分離(CQRS)等等,這些模式的共同特點是,它們對軟體系統的基本組織進行描述,這包括各種組件以及組件之間、組件與環境之間的相互關系的定義,并決定了軟體系統設計與演進的原則,設計模式更多的是在組件內部,對于物件及其之間的關系以及它們之間的行為與協作提供一定的設計準則,從而使得組件的設計滿足面向物件的SOLID原則,慣用法則是與特定編程語言相關的一種常用模式,比如在C#中,對于單例模式(Singleton)有它自己的獨特的實作方式,這種方式依賴于C#中靜態欄位是執行緒安全的語言特性,而這種實作方式卻并不能用在C++中, 與架構模式相比,架構風格并不關心真正的業務領域是什么,以及軟體系統需要解決什么樣的業務問題,無論你是開發ERP系統,還是開發購物網站,你都可以選擇微服務架構,只是不同領域所需要的微服務不同罷了,常見的軟體系統架構風格有:經典分層架構(N-Tier)、事件驅動架構(EDA)以及微服務架構(Microservices),隨著云計算的普及和推進,也衍生出了一些與云計算、人工智能以及大資料處理相關的架構風格,比如基于微軟Azure云平臺的Web-Queue-Worker架構、Big data架構以及Big Compute架構,那么,我到底應該選擇什么樣的架構風格呢?在不同的架構風格下,領域驅動設計又如何運用呢?下面就對比較常見和流行的經典分層架構、事件驅動架構以及微服務架構做一些介紹,

經典分層架構(N-Tier Architecture)

這是一種為人熟知的架構風格,基本上所有開發人員都知道,軟體系統需要分層設計,比較傳統的常見的分層方式就是分三層:界面層、業務邏輯層以及資料訪問層,各層之間會有資料傳輸物件(DTO)完成資料互動,以此隔離不同層內部的實作細節,領域驅動設計則將應用系統分為四層:用戶界面層應用層領域層基礎設施層

  • 用戶界面層:這一層比較好理解,就是直接面向用戶的這一層,比如前端單頁面應用或者基于MVC框架開發的前端應用,如果你的應用系統僅提供API,那么API這一層也屬于用戶界面層
  • 應用層:根據領域驅動設計的描述,應用層是很薄的一層,它主要負責協調下層的執行任務,并隔離領域層與用戶界面層,如果你選擇采用經典分層架構,并開始實踐領域驅動設計,那么在應用層你可以實作一些諸如Coordinator或者Workflow這樣的組件,它們不參與任何領域或者業務相關的操作,僅僅負責協調,最常見的一種實作就是在應用層引入事務處理,有時候甚至還會跨資源實作分布式事務
  • 領域層:你的領域模型所涉及的所有物件都會出現在這一層,如上文所述,領域層物件需要盡量避免貧血模型,開發團隊與領域專家一起完成領域層的設計與開發任務
  • 基礎設施層:所有與技術細節相關的基礎設施組件都屬于這一層,因此,系統所依賴的資料庫存盤以及外部服務,都屬于基礎設施層,此外還有面向切面(Aspect-Oriented)的組件,比如例外處理模塊、快取模塊、安全模塊等等,也都屬于基礎設施層

在早10年以前,微軟的西班牙團隊在Github上開源了一套完整的基于領域驅動設計實踐的分層架構案例:Microsoft NLayerApp,然而非常可惜的是,這個專案目前已經找不到了,但我仍然保留了一些資料,下圖就是這個NLayerApp的架構圖:

上圖中紅色部分代表的是用戶界面層;天藍色部分代表的是應用層;藍色部分代表的是領域層;而綠色部分則代表基礎設施層,整個軟體的架構是非常清晰的,這就是一個標準的符合領域驅動設計思想的分層架構,在這個案例中,設計者引入了很多體系結構模式,比如領域層的倉儲(Repository)模式和規約(Specification)模式、展現層(用戶界面層)的MVC模式等,還引入了一些開發方法論,比如面向切面的編程(Aspect Oriented Programming, AOP),從整個結構上看,它本身也就是一種架構模式:如果你選擇分層架構風格,那么你就可以考慮使用上圖中類似的結構來開發你的軟體系統,比如引入領域模型、倉儲模式、查詢規約、作業流、MVC等等,當然,分層架構并不一定非要按上圖中的這樣去設計,你可以拋開領域驅動設計思想,自己根據專案或者產品的特點來實作分層,這是完全沒有問題的,只要能夠在一定的成本下,滿足業務領域的需求就可以了, 在分層架構中應用領域驅動設計也是需要經過嚴格推敲和思考的,比如在上圖中,倉儲模式的實作,為什么Repository Contracts(也就是我們平時所說的倉儲介面)是設計在領域層,而Repository Implementations則是放在基礎結構層?原因很簡單:一方面,根據上文所述,倉儲的概念就是管理領域聚合的生命周期,因此它是一個領域模型中的概念,而另一方面,在實際實作當中,倉儲是需要直接訪問資料持久化機制的,而資料持久化機制又是與基礎設施相關的組件,所以,倉儲的實作部分是需要設計在基礎設施層的,于是,領域模型層以及其上層的組件通過倉儲介面訪問倉儲實體,而倉儲實體則是在應用程式啟動的時候通過依賴注入的形式提供, Microsoft NLayer App已經不存在了,不過你也可以參考我在很早以前寫的一個符合領域驅動設計的多層分布式架構案例:Byteart Retail,雖然目前看起來它所使用的技術相對比較老,但是整個系統的架構和各層組織結構還是非常清晰的,基本上可以比對上圖的架構去閱讀了解, 至此,你應該對領域驅動設計是如何在分層架構中運用已經有了一定的了解,你會發現,即使是在相對簡單的分層架構中,要正確運用領域驅動設計的思想也不是一件容易的事情,你可以退而求其次,仍然選擇使用分層架構,在對業務領域、研發團隊、專案流程、市場反饋等等各方面進行了綜合評估之后,如果你仍然選擇了分層架構,而并不覺得它是一種不那么流行的架構風格的話,那么恭喜你,你或許做出了一個正確的選擇, 總結起來,分層架構是相對比較簡單比較容易理解的一種架構風格,實踐技術也都非常成熟,有極為成熟的案例可以參考,如果你的軟體系統業務本身并不復雜,而且在將來的一段時間內業務擴展不會特別大(比如為學校圖書館開發一套圖書館管理系統),而你的團隊對于分層架構也更為熟悉的話,它的確是一個不錯的選擇,但是,如果你的軟體所要處理的業務比較復雜,而且今后業務會不斷擴展變大,那么龐大的業務體量將會使得你的業務邏輯層變得臃腫復雜,從而引起系統難以維護、代碼構建時間過長、組件關聯錯綜復雜、系統性能逐漸降低等等一系列問題,在這種情況下,你或許更應該選擇微服務架構風格,但不管怎么選,由領域驅動設計所指導的領域建模實踐以及相關的體系結構模式,都可以使用在(或者不使用在)你所選擇的軟體架構之中, 分層架構大致就介紹這么多吧,接下來介紹一下一種比較流行的架構風格:事件驅動型架構,

事件驅動型架構(Event-Driven Architecture)

事件驅動型架構通過采用一種發布者-訂閱者(Publisher-Subscriber)或者事件流的模型,以異步的形式表達組件之間的關系,在這種架構中,事件產生方生成并發布事件到事件總線(Event Bus),而事件消費方則偵聽事件總線并處理它所關心的事件,事件可以被一個或多個消費者所訂閱和消費,因此,在事件驅動型架構中,事件產生方并不依賴于事件消費方,事件消費方之間也沒有依賴關系,通常情況下,如果你的軟體系統需要執行一些比較耗時的任務,而同時又要保證系統回應度的情況下,可以考慮采用事件驅動型架構,比如,IoT系統通常會采用這種架構,因為資料采集與分析都是比較耗時的操作,客戶端可以首先發起一個創建資料處理任務的操作,然后通過輪詢的方式獲得任務的執行狀態, 由于在這種架構中,各組件都是相互獨立的,因此,這種架構具有很好的延展性(Scalability)和分布式部署的特性;但是,它也有一些實踐上的難點,比如:如何確保事件能夠被準確、穩定地分發;如何確保事件能夠按照一定的順序被消費方消費;如何確保事件僅被同一消費方消費一次等等,舉個例子:在命令查詢職責分離(CQRS)體系結構模式的實踐中,當一個聚合需要被創建的時候,比如當需要創建一個Student聚合時,從Command這一方可能會產生并發布兩個事件:StudentCreatedEvent和StudentNameChangedEvent,分別表示有一個Student聚合已經被創建,并修改了它的Name屬性,那么對于事件的訂閱方,肯定是希望首先處理StudentCreatedEvent,然后處理StudentNameChangedEvent,如果順序反了,那就不對了:Student還沒有被創建出來,又談何修改它的Name屬性呢?如果你的訊息訂閱方只有一個實體在運行,你或許可以通過事件的時間戳或者序列號來確定它們的順序,然后引入一些類似有限狀態機(FSM)的機制來保證訊息的順序消費,但如果(其實是絕大多數情況下)你的訊息訂閱方有多個實體同時運行,那么類似這樣的問題就會變得更加復雜,再比如,很多事件驅動系統中,會通過引入成熟的第三方解決方案來確保事件分發的準確性,以保證當消費方沒有確切給出一個信號的時候,事件一直都能夠被保存在事件總線上以待下一次派發;而對于事件消費方,也會采用一些冪等設計,來保證事件僅被有效處理一次, 接下來我們看一個案例:一個基于命令查詢職責分離(Command Query Responsibility Seggregation)體系結構模式所實作的分布式事件驅動型架構,在這個案例中,你可以了解到領域驅動設計是如何指導其設計并被運用在CQRS體系結構模式當中,CQRS體系結構模式最早是由領域驅動設計先鋒Greg Young提出,它的架構圖大致如下:

(上圖來自2018年1月我在微軟MVP論壇上的講義,主題是《ASP.NET Core下領域驅動設計的實踐》)

 

在CQRS中,所有的操作都是基于事件的,當客戶端發起一個請求需要修改領域物件中的某個屬性時,客戶端會將修改屬性的命令訊息發送到系統中,命令處理器接收到命令訊息之后,會根據聚合根的識別符號(ID),從倉儲中讀取該聚合的所有事件,并根據這些事件重建聚合,在修改了屬性之后,領域模型會產生一個事件,然后將這個事件保存到倉儲中,與此同時,該事件還會被派送到事件訊息總線,這種事件在CQRS模式中稱為領域事件(Domain Events),因為它發生在領域層,接下來,事件處理器在收到屬性修改的領域事件后,會相應地更新查詢資料庫;抑或會觸發內部的有限狀態機,以便在某些情況下當相關聯的領域事件全部被接收之后,能夠重新產生一條命令,對領域模型進行進一步的修改(比如訂單在收到用戶的支付之后,狀態由WaitForPayment改為Paid),這種讀寫分離的架構隔離了領域模型的修改部分與查詢部分,使得它們能夠以異構的平臺和技術被開發和部署,甚至可以以不同的設計策略和資源分配對這兩部分進行獨立設計,此外,CQRS模式存盤了整個系統從運行之初到當前的所有領域事件,也就是說它記錄了整個系統從運行之初到當前所發生過的一切事情,這就使系統具有回溯到任何一個狀態點的能力,這種機制我們通常稱之為事件溯源(Event Sourcing), 從領域驅動設計的角度,CQRS模式中也包含領域模型、倉儲等概念,然而,實作方式與分層架構大不相同:

  1. 領域模型中不包含規約(Specifications),因為“寫”端不具備查詢功能
  2. 領域模型中聚合本身的行為(也就是方法)僅包含一個職責,就是派發領域事件(Domain Events),例如,下面就是修改User聚合的Email屬性的樣例代碼,從代碼上看,它僅僅是派發了一個事件:
  3. 而User聚合本身也是一個事件訂閱者,因此,它在接收到了這個事件后,會更新自己的屬性:

    我相信你肯定會有疑問:這不是多此一舉么?在ChangeEmail方法中直接設定屬性不就行了?然而,答案就是不行,因為當呼叫方通過User ID來向倉儲讀取User聚合的時候,倉儲會從資料庫中讀出與這個ID相關的所有事件,然后逐一應用在User物件上,此時,上面的由InlineEventHandler所標識的HandleChangeEmailEvent事件處理方法就會被呼叫,從而完成對Email屬性的設定,我相信你還會有疑問:倉儲會讀出所有的事件,然后逐一應用在User物件上?那如果與User物件相關的事件特別多,逐一應用這些事件豈不是會影響性能?在CQRS中,這一問題是通過快照解決的,基本思路就是在保存領域事件的時候,每隔一定數量的領域事件對聚合做一次快照,比如每1000個領域事件做一次快照,那么當我們需要恢復第1001個領域事件時,只需要讀出這個快照,然后應用剩下的那一個領域事件即可,并不存在性能問題
  4. 領域模型中物體物件的屬性都是只讀的,因為修改需要通過領域事件來完成
  5. 倉儲中不包含查詢方法,因此,它僅有兩個職責:保存聚合、根據聚合根的ID來讀取聚合:
  6. 倉儲所依賴的事件存盤資料庫中僅有一張資料表(或者說一種檔案):領域事件表,大致包含這些資訊:序列號、領域事件所發生的物件型別、領域事件所發生的物件ID、領域事件型別、領域事件發生時間以及領域事件的具體內容

多年前我也基于CQRS體系結構模式做了一個相對完整的案例:WeText,代碼完全公開在Github,雖說使用的技術可能有些過時,但整個架構是事件驅動型的,并在一定程度上實作了CQRS體系結構模式以及領域驅動設計中的基本要素,這個案例的架構圖如下,供參考:

(圖片來源:本人的開源專案WeText,點擊查看大圖)

 

CQRS模式的實作非常復雜,所以大多數情況下它只會被運用在某個界定背景關系(Bounded Context)中,甚至大多數情況下都不會完整地實作上圖所述的整個結構,或許你的業務并不需要保存歷史事件,那么你就沒必要設計事件存盤;或許你的客戶端不希望以異步的形式向系統發出命令,那么你有可能就不需要命令訊息總線,目前在世界上的確是有完整實作CQRS模式的事件驅動型軟體專案,但卻是鳳毛麟角, 同理,在事件驅動型架構中,并不一定需要采用CQRS架構模式(應該說絕大多數情況下不需要),還是那句話,你應該根據專案本身的特點以及研發團隊的情況來決定使用哪些架構模式來實作事件驅動型架構,有時候你可能只需要一個非常簡單的設計就能滿足要求,但是,如果你希望在事件驅動型架構中實踐領域驅動設計,那么CQRS應該是你所需要了解并深入學習的一種架構模式,它能更好地幫助你理解領域驅動設計,并在事件驅動型架構中更好地運用它, 下面我們再看看目前最為流行的架構風格:微服務架構,

微服務架構(Microservices Architecture)

在最開始著手軟體系統的設計時,你或許不會選擇微服務架構,因為在那個時候,微服務架構并不能幫你解決眼前的設計問題,但是當你的業務領域變得十分龐大,而分層架構無法繼續支撐你的軟體系統時,你可能會考慮采用微服務架構,在微服務架構中,各應用服務之間互相獨立,它們可以由不同團隊采用異構的平臺和技術,以及使用不同的軟體開發方法完成開發,這些服務可以使用不同的資料存盤系統,甚至可以是一個僅進行資料實時處理而不存盤任何資料的計算服務,微服務實體之間可以以同步或者異步的方式進行通訊,不難看出,實踐微服務架構的一個難點就是如何去協調各個服務之間的協作,例如如何在分布式的環境中保證資料的一致性;然而,當你真的決定采用微服務架構時,你所遇到的第一個問題就是:如何劃分微服務的邊界, 在微服務架構的官方網站上給出了四種將應用程式解構成多個微服務的模式:Decompose by business capability、Decompose by subdomain、Self-contained service以及Service per team,其中與領域驅動設計所對應的模式就是Decompose by subdomain,它要求設計者能夠根據軟體系統的業務領域來區分子領域,然后應用相關模式來確定微服務的劃分,大致流程如下:

  1. 對業務領域進行分析,通過通用語言來描述業務領域中的關鍵概念和業務行為,并確定整個大的業務領域由哪些子領域(subdomain)構成
  2. 根據這些子領域來確定界定背景關系(Bounded Context),每一個界定背景關系會有一套獨立的領域模型對子領域進行描述,界定背景關系中的領域模型不會存在二義性
  3. 在界定背景關系中建模,設計好領域模型以及各領域物件之間的關系
  4. 基于建立好的領域模型,劃分微服務

在領域驅動設計中,界定背景關系(有些文章將其翻譯為“有界背景關系”,意思相同)是實作通用語言的重要工具,很多情況下,有些詞語或者句子在不同的背景關系中會有不同的含義,界定背景關系就定義了這樣一個邊界,它能使得在邊界內的詞語或者句子具有唯一明確的含義而不存在二義性,例如某公司生產產品然后賣給客戶(Customer),然后會有另一個團隊為這些客戶(Customer)提供售后服務或技術支持,那么在這里我們有兩個“客戶”的概念,對于整個公司來說,它們表示的是同一個概念,然而在不同的背景關系中,這個“客戶”的概念又有所不同:在銷售子領域中,“客戶”表示產品銷售的物件,因此會更多地關注它對產品的需求以及信用額度、交貨方式等等;而在售后服務子領域中,“客戶”表示提供服務的物件,因此會更多關注它的歷史訂單資訊以及歷史服務工單,從上面的基于Decompose by subdomain的基本流程來看,一旦區分并確定了整個領域中的界定背景關系,也就基本上確定了應用系統中大致會有哪些微服務, 從領域模型上分析,界定背景關系也不是絕對獨立的,應該說絕大多數情況下不是,領域驅動設計引入了“背景關系映射(Context Mapping)”來解決跨界定背景關系的領域知識的互動,常用的方式可以是使負責不同子領域的團隊之間達成共識、通過抽象手段來建立跨多個界定背景關系的公共模型(Shared Kernel),或者引入防腐層(Anti-corruption Layer)來達到不同界定背景關系之間無縫溝通的目的,這篇文章很好地介紹了這些內容, 這里限于文章篇幅,我僅僅簡單地介紹了與領域驅動設計相關的要點,上面討論的內容中的每一個點都可以繼續展開討論繼續分析研究,你是不是已經開始考慮是否真的需要微服務架構了吧?因為是否采用微服務架構風格,以及微服務如何劃分,將直接影響到今后你的業務系統的開發和演進是否真的能夠幫你解決龐大的業務領域體量所帶來的軟體開發問題,而不是讓你的架構變得逐漸臃腫不堪錯誤百出難以維護,給你帶來無窮無盡的煩惱, 或許你的應用系統并沒有那么大的業務領域體量,你也已經將你的業務領域劃分成了多個微服務,那么接下來就是開發技術以及開發流程和團隊管理的問題了,微服務架構真的有很多優點:由于整個業務領域被劃分成多個子領域,由不同的微服務實作,因此這種架構風格具有非常好的延展性,并且可以根據需要來動態調配各個服務的運行資源,另一方面,在微服務架構中,通常都會由不同的團隊來負責各個微服務的開發,這些團隊可以選擇合適的技術,采用自己的代碼托管與分支策略,使用不同的軟體開發程序來開展開發任務,如果團隊采用敏捷開發程序,那么一個相對較小的團隊能夠更加高效地實踐敏捷,使得微服務的開發能夠不斷向前迭代,微服務架構的另一個優點就是對于云平臺的支持,雖然各個服務會采用不同技術運行在不同平臺上,然而現在流行的容器化技術可以屏蔽這種應用層技術實作的差異,通過將各個服務封裝成容器,使得整個應用系統可以非常方便地部署到云平臺,并且非常方便地呼叫托管的云服務, 由于這種架構上的靈活性和分布式的特點,微服務架構也存在很多挑戰:配置管理、服務發現、服務間通信、分布式事務(資料最終一致性的保證)、部署和測驗復雜度、安全策略的實作等等,每一個技術難點都有可能成為你成功實踐微服務架構的阻力,例如,異步通信是微服務間最為常見的通信機制之一,而大多數情況下,分布式事務就需要依賴于這種異步通信機制,而它一般都是基于事件訊息的,所以,除了基本的事件訊息框架的實作之外,各個微服務還需要考慮如何參與到這種分布式事務之中:如何在事務成功的時候提交變更,以及如何在事務失敗的時候進行補償操作,Saga體系結構模式就是一種實作跨服務事務的模式,它有兩種實作方式:編排式協調式,前者通過微服務之間互通領域事件來實作事務,而后者則是由一個中心化的協調器來接收來自各服務的領域事件,然后根據領域事件的處理結果來決定整個事務應該被接識訓是被駁回,當某個事務參與的微服務比較少,并且處理邏輯不復雜的情況下,采用編排式的設計會比較簡單;但如果參與的微服務和領域事件比較多,選擇協調式的設計會使得結構更加清晰,而且不容易出錯,目前有一些開發框架已經很好地實作了或者支持Saga模式,比如.NET下的NServiceBus框架,然而由于其過于復雜,學習成本比較高,因此應用范圍也不是特別廣, 值得一提的是,微服務架構之下各服務之間隔離度越高越好,雖然微服務架構本身并不強制要求每個服務都有自己的資料庫,但是Database per service仍然是一個比較推薦的做法,前端的實作也是如此,開發團隊可以有各自的前端開發人員來開發用于當前微服務的前端界面,然后通過某些微前端框架進行整合, 所以,微服務架構看上去比較先進、時尚,但是要想有效、正確地實踐微服務架構卻不是一件容易的事情,如果你的業務系統并沒有大到需要拆分成多個子系統來進行設計,或者你的團隊沒有大到足以應對由這些微服務帶來的技術復雜度,那么,你真的應該考慮一下,采用微服務的架構是否真的利大于弊,架構設計就是如此,沒有對錯,只有是否合理,整個程序就是平衡與取舍, 以下是微軟官方的一個完整的微服務架構的案例:eShopOnContainers,代碼開源,其業務領域是一個電商零售網站,它的架構圖如下:

(圖片來源:微軟eShopOnContainers代碼庫,點擊查看大圖)

 

eShopOnContainers支持移動客戶端、傳統的基于ASP.NET Core MVC的瀏覽器客戶端以及基于Angular的單頁面應用(SPA)三種不同的客戶端體驗;在服務端,eShopOnContainers實作了面向mobile和面向web的兩套API網關(API Gateway),所有的API請求都由這兩套網關所代理,與后端的不同微服務進行通信,eShopOnContainers采用基于ASP.NET Identity的由IdentityServer4所實作的認證與授權機制,它是一個基于SQL Server資料庫的傳統的ASP.NET Core的服務, 在基于子領域的劃分上,eShopOnContainers將其業務領域分為三個子領域:用于維護商品資訊的Catalog子領域、用于處理訂單的Ordering子領域以及用于管理購物籃資訊的Basket子領域,因此,對應的微服務也就按子領域進行劃分,各個微服務所采用的技術也完全不同:

  • Catalog微服務使用傳統的Data Service/CRUD API模式,將Entity Framework Core的DbContext以構造器注入的方式注入控制器(Controller),然后在控制器中完成業務操作和資料訪問,后臺采用SQL Server資料庫
  • Ordering微服務使用CQRS體系結構模式,它的運作完全基于領域事件,雖然它并非完全實作CQRS模式的所有細節,但已經足夠實作它的業務邏輯,并且它的復雜度也得到了很好的控制,它后臺也是采用SQL Server資料庫
  • Basket微服務使用基于領域驅動設計的分層模式,它引入了領域模型、倉儲等概念,并將倉儲的實體通過構造器注入的方式注入控制器,然后讓控制器充當領域驅動設計中應用層的角色,完成業務處理和領域模型的重建和持久化,后臺采用Redis快取作為資料持久化機制

這些微服務之間通過RabbitMQ(或者Azure Service Bus)的事件總線(Event Bus)完成通信,以編排式的Saga模式實作了基本的分布式事務,整個后端架構都是容器化的,運行在容器編排集群中(docker-compose或者Kubernetes), 由此可見,在微服務的架構風格中,領域驅動設計能夠被更加靈活地運用,由于不同的微服務是由不同的團隊負責開發,因此就可以在不同的微服務中,以不同的程度來引入領域驅動設計的思想以輔助解決業務分析與系統開發中的難點,最終達到整個軟體架構的良性發展, 軟體架構風格大致就介紹這些吧,涉及的內容確實很多,也沒有辦法在一篇文章里完全寫完,以后有機會再深入補充吧,

總結

讀到這里,你應該已經大致了解了什么是領域驅動設計、軟體架構模式與軟體架構風格的區別是什么、常見的軟體架構風格有哪些,以及在不同的軟體架構風格下,領域驅動設計是如何對軟體的架構設計提供指引并指導模式的合理使用,你還會了解到,很多情況下,對于絕大多數專案而言,或許一個面向資料的CRUD服務已經完全能夠滿足你的應用系統需求,或許你也只需要一個單體架構(Monolithic)就能夠解決你眼下乃至幾年內的開發痛點,那么在這些情況下,你需要慎重考慮是否真的需要“趕時髦”地引入過于復雜的架構風格和架構模式,然而另一方面,在團隊相對比較成熟、對領域驅動設計有一定認知和認同、成本允許的前提下,能夠鼓勵大家嘗試實踐領域驅動設計,這也是一件非常好的事情,畢竟有學習有實踐才會有進步,所以,何時使用領域驅動設計?應該選擇什么樣的架構風格?還是你自己來決定吧,

參考閱讀

借此機會推薦一些非常優秀的“課外讀物”,這些著作的經典程度不亞于《設計模式:可復用面向物件軟體的基礎》(GoF95)一書之于面向物件分析與設計(OOAD)的經典程度,如果有興趣,推薦參考閱讀:

  • 《領域驅動設計:軟體核心復雜性應對之道》:Eric Evans
  • 《企業應用架構模式(PoEAA)》:Martin Fowler
  • 《面向模式的軟體體系結構:卷一:模式系統》
  • 《面向模式的軟體體系結構:卷二:用于并發和網路化物件的模式》
  • 《實作領域驅動設計》:Vaughn Vernon
  • 《微服務架構設計模式》:Chris Richardson
  • 《.NET Microservices:Architecture for Containerized .NET Applications》電子書:Microsoft

此外,我自己也有幾個Github Repo,雖然很久沒有更新了,但當時也是在一定程度上以各種不同的架構風格,采用了不同的架構模式實作了領域驅動設計(在上面文章中也已經提及這些Repo,這里總結一下):

  • 分層架構案例:Byteart Retail:https://github.com/daxnet/byteartretail
  • 事件驅動型架構案例:WeText:https://github.com/daxnet/we-text
  • 基于.NET Core的領域驅動設計開發框架:https://github.com/daxnet/apworks-core

十多年前,我也總結了不少領域驅動設計的文章,完整串列在此,也可以參考了解一下,

 

歡迎訪問本人的個人站點https://sunnycoding.cn,獲得更好的閱讀體驗,

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/296421.html

標籤:領域驅動設計

上一篇:為什么說開源的COLA既是架構也是框架?

下一篇:計算機網路 學習記錄/知識點總結(更新至1.3.1)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more