何時使用領域驅動設計?其實當你的應用程式架構設計是面向業務的時候,你已經開始使用領域驅動設計了。領域驅動設計既不是架構風格(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模式中也包含領域模型、倉儲等概念,然而,實現方式與分層架構大不相同:
- 領域模型中不包含規約(Specifications),因為“寫”端不具備查詢功能
- 領域模型中聚合本身的行為(也就是方法)僅包含一個職責,就是派發領域事件(Domain Events)。例如,下面就是修改User聚合的Email屬性的樣例程式碼,從程式碼上看,它僅僅是派發了一個事件:
- 而User聚合本身也是一個事件訂閱者,因此,它在接收到了這個事件後,會更新自己的屬性:
我相信你肯定會有疑問:這不是多此一舉麼?在ChangeEmail方法中直接設定屬性不就行了?然而,答案就是不行,因為當呼叫方通過User ID來向倉儲讀取User聚合的時候,倉儲會從資料庫中讀出與這個ID相關的所有事件,然後逐一應用在User物件上,此時,上面的由InlineEventHandler所標識的HandleChangeEmailEvent事件處理方法就會被呼叫,從而完成對Email屬性的設定。我相信你還會有疑問:倉儲會讀出所有的事件,然後逐一應用在User物件上?那如果與User物件相關的事件特別多,逐一應用這些事件豈不是會影響效能?在CQRS中,這一問題是通過快照解決的,基本思路就是在儲存領域事件的時候,每隔一定數量的領域事件對聚合做一次快照,比如每1000個領域事件做一次快照,那麼當我們需要恢復第1001個領域事件時,只需要讀出這個快照,然後應用剩下的那一個領域事件即可,並不存在效能問題 - 領域模型中實體物件的屬性都是隻讀的,因為修改需要通過領域事件來完成
- 倉儲中不包含查詢方法,因此,它僅有兩個職責:儲存聚合、根據聚合根的ID來讀取聚合:
- 倉儲所依賴的事件儲存資料庫中僅有一張資料表(或者說一種文件):領域事件表,大致包含這些資訊:序列號、領域事件所發生的物件型別、領域事件所發生的物件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,它要求設計者能夠根據軟體系統的業務領域來區分子領域,然後應用相關模式來確定微服務的劃分,大致流程如下:
- 對業務領域進行分析,通過通用語言來描述業務領域中的關鍵概念和業務行為,並確定整個大的業務領域由哪些子領域(subdomain)構成
- 根據這些子領域來確定界定上下文(Bounded Context),每一個界定上下文會有一套獨立的領域模型對子領域進行描述,界定上下文中的領域模型不會存在二義性
- 在界定上下文中建模,設計好領域模型以及各領域物件之間的關係
- 基於建立好的領域模型,劃分微服務
在領域驅動設計中,界定上下文(有些文章將其翻譯為“有界上下文”,意思相同)是實現通用語言的重要工具,很多情況下,有些詞語或者句子在不同的上下文中會有不同的含義,界定上下文就定義了這樣一個邊界,它能使得在邊界內的詞語或者句子具有唯一明確的含義而不存在二義性。例如某公司生產產品然後賣給客戶(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,獲得更好的閱讀體驗。