天天看點

還在搞三層架構?DDD 分層架構了解下!

引言

在讨論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在《領域驅動設計-軟體核心複雜性應對之道》這本書中提出了傳統的四層架構模式,如下圖所示:

還在搞三層架構?DDD 分層架構了解下!

User Interface為使用者界面層(或表示層),負責向使用者顯示資訊和解釋使用者指令。這裡指的使用者可以是另一個計算機系統,不一定是使用使用者界面的人。

Application為應用層,定義軟體要完成的任務,并且指揮表達領域概念的對象來解決問題。這一層所負責的工作對業務來說意義重大,也是與其它系統的應用層進行互動的必要管道。應用層要盡量簡單,不包含業務規則或者知識,而隻為下一層中的領域對象協調任務,配置設定工作,使它們互相協作。它沒有反映業務情況的狀态,但是卻可以具有另外一種狀态,為使用者或程式顯示某個任務的進度。

Domain為領域層(或模型層),負責表達業務概念,業務狀态資訊以及業務規則。盡管儲存業務狀态的技術細節是由基礎設施層實作的,但是反映業務情況的狀态是由本層控制并且使用的。領域層是業務軟體的核心,領域模型位于這一層。

Infrastructure層為基礎實施層,向其他層提供通用的技術能力:為應用層傳遞消息,為領域層提供持久化機制,為使用者界面層繪制螢幕元件,等等。基礎設施層還能夠通過架構架構來支援四個層次間的互動模式。 傳統的四層架構都是 限定型松散分層架構 ,即Infrastructure層的任意上層都可以通路該層(“L”型),而其它層遵守 嚴格分層架構

筆者在四層架構模式的實踐中,對于分層的本地化定義主要為:

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。

還在搞三層架構?DDD 分層架構了解下!

DCI中的Context層從Domain層上移變成Context層。 是以,DDD分層架構模式就變成了五層,如下圖所示:

還在搞三層架構?DDD 分層架構了解下!

筆者在實踐中,将這五層的本地化定義為:

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 分層架構了解下!

筆者在實踐中,将這六層的本地化定義為:

User Interface是使用者接口層,主要用于處理使用者發送的Restful請求和解析使用者輸入的配置檔案等,并将資訊傳遞給Scheduler層的接口。

Scheduler是排程層,負責多程序管理及排程、多線程管理及排程、多協程排程和維護業務執行個體的狀态模型。當排程層收到使用者接口層的請求後,委托Transaction層與本次操作相關的事務進行處理。

Transaction是事務層,對應一個業務流程,比如UE Attach,将多個同步消息或異步消息的處理序列組合成一個事務,而且在大多場景下,都有選擇結構。萬一事務執行失敗,則立即進行復原。當事務層收到排程層的請求後,委托Context層的Action進行處理,常常還伴随使用Context層的Specification(謂詞)進行Action的選擇。

Context是環境層,以Action為機關,處理一條同步消息或異步消息,将Domain層的領域對象cast成合适的role,讓role互動起來完成業務邏輯。環境層通常也包括Specification的實作,即通過Domain層的知識去完成一個條件判斷。

事務層的核心是事務模型,事務模型的架構代碼一般放在基礎設施層。關于事務模型,筆者以前分享過一篇文章— 《Golang事務模型》 ,感興趣的同學可以看看。

綜上所述,DDD六層架構可以看做是DDD五層架構在特定領域的變體,我們統稱為DDD五層架構,而DDD五層架構與傳統的四層架構類似,都是限定型松散分層架構 。

模式三:六邊形架構

有一種方法可以改進分層架構,即依賴倒置原則(Dependency Inversion Principle,DIP),它通過改變不同層之間的依賴關系達到改進目的。

依賴倒置原則由Robert C. Martin提出,正式定義為: 高層子產品不應該依賴于底層子產品,兩者都應該依賴于抽象。 抽象不應該依賴于細節,細節應該依賴于抽象。

根據該定義,DDD分層架構中的低層元件應該依賴于高層元件提供的接口,即無論高層還是低層都依賴于抽象,整個分層架構好像被推平了。如果我們把分層架構推平,再向其中加入一些對稱性,就會出現一種具有對稱性特征的架構風格,即六邊形架構。六邊形架構是AlistairCockburn在2005年提出的,在這種架構中,不同的客戶通過“平等”的方式與系統互動。需要新的客戶嗎?不是問題。隻需要添加一個新的擴充卡将客戶輸入轉化成能被系統API所了解的參數就行。同時,對于每種特定的輸出,都有一個建立的擴充卡負責完成相應的轉化功能。

六邊形架構也稱為端口與擴充卡,如下圖所示:

還在搞三層架構?DDD 分層架構了解下!

六邊形每條不同的邊代表了不同類型的端口,端口要麼處理輸入,要麼處理輸出。對于每種外界類型,都有一個擴充卡與之對應,外界通過應用層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分層架構模式,進而傳遞結構清晰且易維護的軟體産品。

繼續閱讀