還在搞三層架構?瞭解下 DDD 分層架構的三種模式吧 !
_來源:https://www.jianshu.com/p/a775836c7e25
_
引言
在討論 DDD 分層架構的模式之前,我們先一起回顧一下 DDD 和分層架構的相關知識。
DDD
DDD(Domain DrivenDesign,領域驅動設計)作爲一種軟件開發方法,它可以幫助我們設計高質量的軟件模型。在正確實現的情況下,我們通過 DDD 完成的設計恰恰就是軟件的工作方式。
UL(Ubiquitous Language,通用語言)是團隊共享的語言,是 DDD 中最具威力的特性之一。不管你在團隊中的角色如何,只要你是團隊的一員,你都將使用 UL。由於 UL 的重要性,所以需要讓每個概念在各自的上下文中是清晰無歧義的,於是 DDD 在戰略設計上提出了模式 BC(BoundedContext,限界上下文)。UL 和 BC 同時構成了 DDD 的兩大支柱,並且它們是相輔相成的,即 UL 都有其確定的上下文含義,而 BC 中的每個概念都有唯一的含義。
一個業務領域劃分成若干個 BC,它們之間通過 Context Map 進行集成。BC 是一個顯式的邊界,領域模型便存在於這個邊界之內。領域模型是關於某個特定業務領域的軟件模型。通常,領域模型通過對象模型來實現,這些對象同時包含了數據和行爲,並且表達了準確的業務含義。
從廣義上來講,領域即是一個組織所做的事情以及其中所包含的一切,表示整個業務系統。由於 “領域模型” 包含了 “領域” 這個詞,我們可能會認爲應該爲整個業務系統創建一個單一的、內聚的和全功能式的模型。然而,這並不是我們使用 DDD 的目標。正好相反,領域模型存在於 BC 內。
在微服務架構實踐中,人們大量地使用了 DDD 中的概念和技術:
-
- 微服務中應該首先建立 UL,然後再討論領域模型。
-
- 一個微服務最大不要超過一個 BC,否則微服務內會存在有歧義的領域概念。
-
- 一個微服務最小不要小於一個聚合,否則會引入分佈式事務的複雜度。
-
- 微服務的劃分過程類似於 BC 的劃分過程,每個微服務都有一個領域模型。
-
- 微服務間的集成可以通過 Context Map 來完成,比如 ACL(Anticorruption Layer,防腐層)。
-
- 微服務間最好採用 Domain Event(領域事件)來進行交互,使得微服務可以保持松耦合。
-
- …
分層架構
分層架構的一個重要原則是每層只能與位於其下方的層發生耦合。分層架構可以簡單分爲兩種,即嚴格分層架構和鬆散分層架構。在 嚴格分層架構中,某層只能與位於其直接下方的層發生耦合,而在 鬆散分層架構 中,則允許某層與它的任意下方層發生耦合。
分層架構的好處是顯而易見的。首先,由於層間鬆散的耦合關係,使得我們可以專注於本層的設計,而不必關心其他層的設計,也不必擔心自己的設計會影響其它層,對提高軟件質量大有裨益。其次,分層架構使得程序結構清晰,升級和維護都變得十分容易,更改某層的具體實現代碼,只要本層的接口保持穩定,其他層可以不必修改。即使本層的接口發生變化,也隻影響相鄰的上層,修改工作量小且錯誤可以控制,不會帶來意外的風險。
要保持程序分層架構的優點,就必須堅持層間的鬆散耦合關係。設計程序時,應先劃分出可能的層次,以及此層次提供的接口和需要的接口。設計某層時,應儘量保持層間的隔離,僅使用下層提供的接口。 關於分層架構的優點,Martin Fowler 在《Patterns of Enterprise Application Architecture》一書中給出了答案:
-
- 開發人員可以只關注整個結構中的某一層。
-
- 可以很容易的用新的實現來替換原有層次的實現。
-
- 可以降低層與層之間的依賴。
-
- 有利於標準化。
-
- 利於各層邏輯的複用。
“金無足赤,人無完人”,分層架構也不可避免具有一些缺陷:
-
- 降低了系統的性能。這是顯然的,因爲增加了中間層,不過可以通過緩存機制來改善。
-
- 可能會導致級聯的修改。這種修改尤其體現在自上而下的方向,不過可以通過依賴倒置來改善。
在每個 BC 中爲了凸顯領域模型,DDD 中提出了分層架構模式。最近幾年,筆者在實踐 DDD 的過程中,也經常使用分層架構模式,本文主要分享 DDD 分層架構中比較經典的三種模式。
模式一:四層架構
Eric Evans 在《領域驅動設計-軟件核心複雜性應對之道》這本書中提出了傳統的四層架構模式,如下圖所示:
-
- User Interface 爲用戶界面層(或表示層),負責向用戶顯示信息和解釋用戶命令。這裏指的用戶可以是另一個計算機系統,不一定是使用用戶界面的人。
-
- Application 爲應用層,定義軟件要完成的任務,並且指揮表達領域概念的對象來解決問題。這一層所負責的工作對業務來說意義重大,也是與其它系統的應用層進行交互的必要渠道。應用層要儘量簡單,不包含業務規則或者知識,而只爲下一層中的領域對象協調任務,分配工作,使它們互相協作。它沒有反映業務情況的狀態,但是卻可以具有另外一種狀態,爲用戶或程序顯示某個任務的進度。
-
- Domain 爲領域層(或模型層),負責表達業務概念,業務狀態信息以及業務規則。儘管保存業務狀態的技術細節是由基礎設施層實現的,但是反映業務情況的狀態是由本層控制並且使用的。領域層是業務軟件的核心,領域模型位於這一層。
-
- Infrastructure 層爲基礎實施層,向其他層提供通用的技術能力:爲應用層傳遞消息,爲領域層提供持久化機制,爲用戶界面層繪製屏幕組件,等等。基礎設施層還能夠通過架構框架來支持四個層次間的交互模式。
傳統的四層架構都是 限定型鬆散分層架構 ,即 Infrastructure 層的任意上層都可以訪問該層(“L” 型),而其它層遵守 嚴格分層架構
- Infrastructure 層爲基礎實施層,向其他層提供通用的技術能力:爲應用層傳遞消息,爲領域層提供持久化機制,爲用戶界面層繪製屏幕組件,等等。基礎設施層還能夠通過架構框架來支持四個層次間的交互模式。
筆者在四層架構模式的實踐中,對於分層的本地化定義主要爲:
-
- User Interface 層主要是 Restful 消息處理,配置文件解析,等等。
-
- Application 層主要是多進程管理及調度,多線程管理及調度,多協程調度和狀態機管理,等等。
-
- Domain 層主要是領域模型的實現,包括領域對象的確立,這些對象的生命週期管理及關係,領域服務的定義,領域事件的發佈,等等。
-
- Infrastructure 層主要是業務平臺,編程框架,第三方庫的封裝,基礎算法,等等。
說明:嚴格意義上來說,User Interface 指的是用戶界面,Restful 消息和配置文件解析等處理應該放在 Application 層,User Interface 層沒有的話就空缺。但 User Interface 也可以理解爲用戶接口,所以將 Restful 消息和配置文件解析等處理放在 User Interface 層也行。
模式二:五層架構
James O. Coplien 和 Trygve Reenskaug 在 2009 年發表了一篇論文《DCI 架構:面向對象編程的新構想》,標誌着 DCI 架構模式的誕生。有趣的是 James O.Coplien 也是 MVC 架構模式的創造者,這個大叔一輩子就幹了兩件事,即年輕時創造了 MVC 和年老時創造了 DCI,其他時間都在思考,讓我輩望塵莫及。
面向對象編程的本意是將程序員與用戶的視角統一於計算機代碼之中:對提高可用性和降低程序的理解難度來說,都是一種恩賜。可是雖然對象很好地反映了結構,但在反映系統的動作方面卻失敗了,DCI 的構想是期望反映出最終用戶的認知模型中的角色以及角色之間的交互。
傳統上,面向對象編程語言拿不出辦法去捕捉對象之間的協作,反映不了協作中往來的算法。就像對象的實例反映出領域結構一樣,對象的協作與交互同樣是有結構的。協作與交互也是最終用戶心智模型的組成部分,但你在代碼中找不到一個內聚的表現形式去代表它們。在本質上,角色體現的是一般化的、抽象的算法。角色沒有血肉,並不能做實際的事情,歸根結底工作還是落在對象的頭上,而對象本身還擔負着體現領域模型的責任。
人們心目中對 “對象” 這個統一的整體卻有兩種不同的模型,即 “系統是什麼” 和“系統做什麼”,這就是 DCI 要解決的根本問題。用戶認知一個個對象和它們所代表的領域,而每個對象還必須按照用戶心目中的交互模型去實現一些行爲,通過它在用例中所扮演的角色與其他對象聯結在一起。正因爲最終用戶能把兩種視角合爲一體,類的對象除了支持所屬類的成員函數,還可以執行所扮演角色的成員函數,就好像那些函數屬於對象本身一樣。換句話說,我們希望把角色的邏輯注入到對象,讓這些邏輯成爲對象的一部分,而其地位卻絲毫不弱於對象初始化時從類所得到的方法。我們在編譯時就爲對象安排好了扮演角色時可能需要的所有邏輯。如果我們再聰明一點,在運行時才知道了被分配的角色,然後注入剛好要用到的邏輯,也是可以做到的。
算法及角色 - 對象映射由 Context 擁有。Context“知道”在當前用例中應該找哪個對象去充當實際的演員,然後負責把對象 “cast” 成場景中的相應角色(cast 這個詞在戲劇界是選角的意思,此處的用詞至少符合該詞義,另一方面的用意是聯想到 cast
在某些編程語言類型系統中的含義)。在典型的實現裏,每個用例都有其對應的一個 Context 對象,而用例涉及到的每個角色在對應的 Context 裏也都有一個標識符。Context 要做的只是將角色標識符與正確的對象綁定到一起。然後我們只要觸發 Context 裏的 “開場” 角色,代碼就會運行下去。
於是我們有了完整的 DCI 架構(Data、Context 和 Interactive 三層架構):
-
- Data 層描述系統有哪些領域概念及其之間的關係,該層專注於領域對象的確立和這些對象的生命週期管理及關係,讓程序員站在對象的角度思考系統,從而讓 “系統是什麼” 更容易被理解。
-
- Context 層:是儘可能薄的一層。Context 往往被實現得無狀態,只是找到合適的 role,讓 role 交互起來完成業務邏輯即可。但是簡單並不代表不重要,顯示化 context 層正是爲人去理解軟件業務流程提供切入點和主線。
-
- Interactive 層主要體現在對 role 的建模,role 是每個 context 中複雜的業務邏輯的真正執行者,體現 “系統做什麼”。role 所做的是對行爲進行建模,它聯接了 context 和領域對象。由於系統的行爲是複雜且多變的,role 使得系統將穩定的領域模型層和多變的系統行爲層進行了分離,由 role 專注於對系統行爲進行建模。該層往往關注於系統的可擴展性,更加貼近於軟件工程實踐,在面向對象中更多的是以類的視角進行思考設計。
-
DCI 目前廣泛被看作是對 DDD 的一種發展和補充,用在基於面向對象的領域建模上。顯式的對 role 進行建模,解決了面向對象建模中的充血模型和貧血模型之爭。DCI 通過顯式的用 role 對行爲進行建模,同時讓 role 在 context 中可以和對應的領域對象進行綁定 (cast),從而既解決了數據邊界和行爲邊界不一致的問題,也解決了領域對象中數據和行爲高內聚低耦合的問題。
面向對象建模面臨的一個棘手問題是數據邊界和行爲邊界往往不一致。遵循模塊化的思想,我們通過類將行爲和其緊密耦合的數據封裝在一起。但是在複雜的業務場景下,行爲往往跨越多個領域對象,這樣的行爲如果放在某一個對象中必然會導致別的對象需要向該對象暴漏其內部狀態。所以面向對象發展的後來,領域建模出現兩種派別之爭,一種傾向於將跨越多個領域對象的行爲建模在領域服務中。如果這種做法使用過度,則會導致領域對象變成只提供一堆 get 方法的啞對象,這種建模結果被稱之爲貧血模型。而另一派則堅定的認爲方法應該屬於領域對象,所以所有的業務行爲仍然被放在領域對象中,這樣導致領域對象隨着支持的業務場景變多而變成上帝類,而且類內部方法的抽象層次很難一致。另外由於行爲邊界很難恰當,導致對象之間數據訪問關係也比較複雜,這種建模結果被稱之爲充血模型。
關於多角色對象,舉個生活中的例子:
人有多重角色,不同的角色履行的職責不同:
-
- 作爲父母:我們要給孩子講故事,陪他們玩遊戲,哄它們睡覺。
-
- 作爲子女:我們要孝敬父母,聽取他們的人生建議。
-
- 作爲下屬:我們要服從上司的工作安排,並高質量完成任務。
-
- 作爲上司:我們要安排下屬的工作,並進行培養和激勵。
-
- …
這裏人(大對象)聚合了多個角色(小類),人在某種場景下,只能扮演特定的角色:
-
- 在孩子面前,我們是父母。
-
- 在父母面前,我們是子女。
-
- 在上司面前,我們是下屬。
-
- 在下屬面前,我們是上司。
-
- …
引入 DCI 後,DDD 四層架構模式中的 Domain 層變薄了,以前 Domain 層對應 DCI 中的三層,而現在:
-
- Domain 層只保留了 DCI 中的 Data 層和 Interaction 層,我們在實踐中通常將這兩層使用目錄隔離,即通過兩個目錄 object 和 role 來分離層 Data 和 Interaction。
object-role-dir.png
- DCI 中的 Context 層從 Domain 層上移變成 Context 層。
因此,DDD 分層架構模式就變成了五層,如下圖所示:
ddd-l5.png
筆者在實踐中,將這五層的本地化定義爲:
-
- User Interface 是用戶接口層,主要用於處理用戶發送的 Restful 請求和解析用戶輸入的配置文件等,並將信息傳遞給 Application 層的接口。
-
- Application 層是應用層,負責多進程管理及調度、多線程管理及調度、多協程調度和維護業務實例的狀態模型。當調度層收到用戶接口層的請求後,委託 Context 層與本次業務相關的上下文進行處理。
-
- Context 是環境層,以上下文爲單位,將 Domain 層的領域對象 cast 成合適的 role,讓 role 交互起來完成業務邏輯。
-
- Domain 層是領域層,定義領域模型,不僅包括領域對象及其之間關係的建模,還包括對象的角色 role 的顯式建模。
-
- Infrastructure 層是基礎實施層,爲其他層提供通用的技術能力:業務平臺,編程框架,持久化機制,消息機制,第三方庫的封裝,通用算法,等等。
DDD 五層架構模式討論完了嗎?故事還沒有結束…
筆者參與的很多 DDD 落地實踐,都是面向控制面或管理面且消息交互比較多的系統。這類系統的一次業務,包含一組同步消息或異步消息構成的序列,如果都放在 Context 層,會導致該層的代碼比較複雜,於是我們考慮:
-
- Context 層在面向控制面或管理面且消息交互比較多的系統中又分裂成兩層,即 Context 層和大 Context 層。
-
- Context 層處理單位爲 Action,對應一條同步消息或異步消息。
-
- 大 Context 層對應一個事務處理,由一個 Action 序列組成,一般通過 Transaction DSL 實現,所以我們習慣把大 Context 層叫做 Transaction DSL 層。
-
- Application 層在面向控制面或管理面且消息交互比較多的系統中經常會做一些調度相關的工作,所以我們習慣把 Application 層叫做 Scheduler 層。
因此,在面向控制面或管理面且消息交互比較多的系統中,DDD 分層架構模式就變成了六層,如下圖所示:
ddd-l6.png
筆者在實踐中,將這六層的本地化定義爲:
-
- User Interface 是用戶接口層,主要用於處理用戶發送的 Restful 請求和解析用戶輸入的配置文件等,並將信息傳遞給 Scheduler 層的接口。
-
- Scheduler 是調度層,負責多進程管理及調度、多線程管理及調度、多協程調度和維護業務實例的狀態模型。當調度層收到用戶接口層的請求後,委託 Transaction 層與本次操作相關的事務進行處理。
-
- Transaction 是事務層,對應一個業務流程,比如 UE Attach,將多個同步消息或異步消息的處理序列組合成一個事務,而且在大多場景下,都有選擇結構。萬一事務執行失敗,則立即進行回滾。當事務層收到調度層的請求後,委託 Context 層的 Action 進行處理,常常還伴隨使用 Context 層的 Specification(謂詞)進行 Action 的選擇。
-
- Context 是環境層,以 Action 爲單位,處理一條同步消息或異步消息,將 Domain 層的領域對象 cast 成合適的 role,讓 role 交互起來完成業務邏輯。環境層通常也包括 Specification 的實現,即通過 Domain 層的知識去完成一個條件判斷。
-
- Domain 層是領域層,定義領域模型,不僅包括領域對象及其之間關係的建模,還包括對象的角色 role 的顯式建模。
-
- Infrastructure 層是基礎實施層,爲其他層提供通用的技術能力:業務平臺,編程框架,持久化機制,消息機制,第三方庫的封裝,通用算法,等等。
事務層的核心是事務模型,事務模型的框架代碼一般放在基礎設施層。關於事務模型,筆者以前分享過一篇文章— 《Golang 事務模型》 ,感興趣的同學可以看看。
綜上所述,DDD 六層架構可以看做是 DDD 五層架構在特定領域的變體,我們統稱爲 DDD 五層架構,而 DDD 五層架構與傳統的四層架構類似,都是限定型鬆散分層架構 。
模式三:六邊形架構
有一種方法可以改進分層架構,即依賴倒置原則 (Dependency Inversion Principle,DIP),它通過改變不同層之間的依賴關係達到改進目的。
依賴倒置原則由 Robert C. Martin 提出,正式定義爲:
高層模塊不應該依賴於底層模塊,兩者都應該依賴於抽象。
抽象不應該依賴於細節,細節應該依賴於抽象。
根據該定義,DDD 分層架構中的低層組件應該依賴於高層組件提供的接口,即無論高層還是低層都依賴於抽象,整個分層架構好像被推平了。如果我們把分層架構推平,再向其中加入一些對稱性,就會出現一種具有對稱性特徵的架構風格,即六邊形架構。六邊形架構是 Alistair
Cockburn 在 2005 年提出的,在這種架構中,不同的客戶通過 “平等” 的方式與系統交互。需要新的客戶嗎?不是問題。只需要添加一個新的適配器將客戶輸入轉化成能被系統 API 所理解的參數就行。同時,對於每種特定的輸出,都有一個新建的適配器負責完成相應的轉化功能。
六邊形架構也稱爲端口與適配器,如下圖所示:
六邊形每條不同的邊代表了不同類型的端口,端口要麼處理輸入,要麼處理輸出。對於每種外界類型,都有一個適配器與之對應,外界通過應用層 API 與內部進行交互。上圖中有 3 個客戶請求均抵達相同的輸入端口(適配器 A、B 和 C),另一個客戶請求使用了適配器 D。假設前 3 個請求使用了 HTTP 協議(瀏覽器、REST 和 SOAP 等),而後一個請求使用了 AMQP 協議(比如 RabbitMQ)。端口並沒有明確的定義,它是一個非常靈活的概念。無論採用哪種方式對端口進行劃分,當客戶請求到達時,都應該有相應的適配器對輸入進行轉化,然後端口將調用應用程序的某個操作或者嚮應用程序發送一個事件,控制權由此交給內部區域。
應用程序通過公共 API 接收客戶請求,使用領域模型來處理請求。我們可以將 DDD 戰術設計的建模元素 Repository 的實現看作是持久化適配器,該適配器用於訪問先前存儲的聚合實例或者保存新的聚合實例。正如圖中的適配器 E、F 和 G 所展示的,我們可以通過不同的方式實現資源庫,比如關係型數據庫、基於文檔的存儲、分佈式緩存或內存存儲等。如果應用程序向外界發送領域事件消息,我們將使用適配器 H 進行處理。該適配器處理消息輸出,而上面提到的處理 AMQP 消息的適配器則是處理消息輸入的,因此應該使用不同的端口。
我們在實際的項目開發中,不同層的組件可以同時開發。當一個組件的功能明確後,就可以立即啓動開發。由於該組件的用戶有多個,並且這些用戶的側重點不同,所以需要提供多個不同的接口。同時,這些用戶的認識也是不斷深入的,可能會多次重構相關的接口。於是,組件的多個用戶經常會找組件的開發者討論這些問題,無形中降低了組件的開發效率。
我們換一種方式,組件的開發者在明確了組件的功能後就專注於功能的開發,確保功能穩定和高效。組件的用戶自己定義組件的接口(端口),然後基於接口寫測試,並不斷演進接口。在跨層集成測試時,由組件開發者或用戶再開發一個適配器就可以了。
六邊形架構模式的演變
儘管六邊形架構模式已經很好,但是沒有最好只有更好,演變沒有盡頭。在六邊形架構模式提出後的這些年,又依次衍生出三種六邊形架構模式的變體,感興趣的讀者可以點擊鏈接自行學習:
-
- Jeffrey Palermo 在 2008 年提出了 洋蔥架構 ,六邊形架構是洋蔥架構的一個超集。
-
- Robert C. Martin 在 2012 年提出了 乾淨架構 (Clean Architecture),這是六邊形架構的一個變體。
-
- Russ Miles 在 2013 年提出了 Life Preserver 設計,這是一種基於六邊形架構的設計。
小結
本文先和讀者一起回顧了 DDD 和分層架構的相關知識,然後將 DDD 分層架構中常用的三種模式(四層架構、五層架構和六邊形架構)結合實踐經驗分別進行詳細闡述,使得讀者深刻理解 DDD 分層架構模式,以便在微服務的開發實踐中根據具體情況選擇最合適的 DDD 分層架構模式,從而交付結構清晰且易維護的軟件產品。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ngUAfJI77NHN9DzkWZzskA