还在搞三层架构?了解下 DDD 分层架构的三种模式吧
引言
在討論DDD分層架構(gòu)的模式之前,我們先一起回顧一下DDD和分層架構(gòu)的相關(guān)知識。
DDD
DDD(Domain Driven Design,領(lǐng)域驅(qū)動設(shè)計)作為一種軟件開發(fā)方法,它可以幫助我們設(shè)計高質(zhì)量的軟件模型。在正確實現(xiàn)的情況下,我們通過DDD完成的設(shè)計恰恰就是軟件的工作方式。
UL(Ubiquitous Language,通用語言)是團隊共享的語言,是DDD中最具威力的特性之一。不管你在團隊中的角色如何,只要你是團隊的一員,你都將使用UL。由于UL的重要性,所以需要讓每個概念在各自的上下文中是清晰無歧義的,于是DDD在戰(zhàn)略設(shè)計上提出了模式BC(Bounded Context,限接上下文)。UL和BC同時構(gòu)成了DDD的兩大支柱,并且它們是相輔相成的,即UL都有其確定的上下文含義,而BC中的每個概念都有唯一的含義。
一個業(yè)務(wù)領(lǐng)域劃分成若干個BC,它們之間通過Context Map進行集成。BC是一個顯式的邊界,領(lǐng)域模型便存在于這個邊界之內(nèi)。領(lǐng)域模型是關(guān)于某個特定業(yè)務(wù)領(lǐng)域的軟件模型。通常,領(lǐng)域模型通過對象模型來實現(xiàn),這些對象同時包含了數(shù)據(jù)和行為,并且表達了準確的業(yè)務(wù)含義。
從廣義上來講,領(lǐng)域即是一個組織所做的事情以及其中所包含的一切,表示整個業(yè)務(wù)系統(tǒng)。由于“領(lǐng)域模型”包含了“領(lǐng)域”這個詞,我們可能會認為應(yīng)該為整個業(yè)務(wù)系統(tǒng)創(chuàng)建一個單一的、內(nèi)聚的和全功能式的模型。然而,這并不是我們使用DDD的目標。正好相反,領(lǐng)域模型存在于BC內(nèi)。
在微服務(wù)架構(gòu)實踐中,人們大量地使用了DDD中的概念和技術(shù):
分層架構(gòu)
分層架構(gòu)的一個重要原則是每層只能與位于其下方的層發(fā)生耦合。分層架構(gòu)可以簡單分為兩種,即嚴格分層架構(gòu)和松散分層架構(gòu)。在嚴格分層架構(gòu)中,某層只能與位于其直接下方的層發(fā)生耦合,而在松散分層架構(gòu)中,則允許某層與它的任意下方層發(fā)生耦合。
分層架構(gòu)的好處是顯而易見的。首先,由于層間松散的耦合關(guān)系,使得我們可以專注于本層的設(shè)計,而不必關(guān)心其他層的設(shè)計,也不必擔心自己的設(shè)計會影響其它層,對提高軟件質(zhì)量大有裨益。其次,分層架構(gòu)使得程序結(jié)構(gòu)清晰,升級和維護都變得十分容易,更改某層的具體實現(xiàn)代碼,只要本層的接口保持穩(wěn)定,其他層可以不必修改。即使本層的接口發(fā)生變化,也只影響相鄰的上層,修改工作量小且錯誤可以控制,不會帶來意外的風險。
要保持程序分層架構(gòu)的優(yōu)點,就必須堅持層間的松散耦合關(guān)系。設(shè)計程序時,應(yīng)先劃分出可能的層次,以及此層次提供的接口和需要的接口。設(shè)計某層時,應(yīng)盡量保持層間的隔離,僅使用下層提供的接口。
關(guān)于分層架構(gòu)的優(yōu)點,Martin Fowler在《Patterns of Enterprise Application Architecture》一書中給出了答案:
“金無足赤,人無完人”,分層架構(gòu)也不可避免具有一些缺陷:
在每個BC中為了凸顯領(lǐng)域模型,DDD中提出了分層架構(gòu)模式。最近幾年,筆者在實踐DDD的過程中,也經(jīng)常使用分層架構(gòu)模式,本文主要分享DDD分層架構(gòu)中比較經(jīng)典的三種模式。
模式一:四層架構(gòu)
Eric Evans在《領(lǐng)域驅(qū)動設(shè)計-軟件核心復雜性應(yīng)對之道》這本書中提出了傳統(tǒng)的四層架構(gòu)模式,如下圖所示:
ddd-l4.png
傳統(tǒng)的四層架構(gòu)都是限定型松散分層架構(gòu),即Infrastructure層的任意上層都可以訪問該層(“L”型),而其它層遵守嚴格分層架構(gòu)
筆者在四層架構(gòu)模式的實踐中,對于分層的本地化定義主要為:
說明:嚴格意義上來說,User Interface指的是用戶界面,Restful消息和配置文件解析等處理應(yīng)該放在Application層,User Interface層沒有的話就空缺。但User Interface也可以理解為用戶接口,所以將Restful消息和配置文件解析等處理放在User Interface層也行。
模式二:五層架構(gòu)
James O. Coplien和Trygve Reenskaug在2009年發(fā)表了一篇論文《DCI架構(gòu):面向?qū)ο缶幊痰男聵?gòu)想》,標志著DCI架構(gòu)模式的誕生。有趣的是James O. Coplien也是MVC架構(gòu)模式的創(chuàng)造者,這個大叔一輩子就干了兩件事,即年輕時創(chuàng)造了MVC和年老時創(chuàng)造了DCI,其他時間都在思考,讓我輩望塵莫及。
面向?qū)ο缶幊痰谋疽馐菍⒊绦騿T與用戶的視角統(tǒng)一于計算機代碼之中:對提高可用性和降低程序的理解難度來說,都是一種恩賜。可是雖然對象很好地反映了結(jié)構(gòu),但在反映系統(tǒng)的動作方面卻失敗了,DCI的構(gòu)想是期望反映出最終用戶的認知模型中的角色以及角色之間的交互。
傳統(tǒng)上,面向?qū)ο缶幊陶Z言拿不出辦法去捕捉對象之間的協(xié)作,反映不了協(xié)作中往來的算法。就像對象的實例反映出領(lǐng)域結(jié)構(gòu)一樣,對象的協(xié)作與交互同樣是有結(jié)構(gòu)的。協(xié)作與交互也是最終用戶心智模型的組成部分,但你在代碼中找不到一個內(nèi)聚的表現(xiàn)形式去代表它們。在本質(zhì)上,角色體現(xiàn)的是一般化的、抽象的算法。角色沒有血肉,并不能做實際的事情,歸根結(jié)底工作還是落在對象的頭上,而對象本身還擔負著體現(xiàn)領(lǐng)域模型的責任。
人們心目中對“對象”這個統(tǒng)一的整體卻有兩種不同的模型,即“系統(tǒng)是什么”和“系統(tǒng)做什么”,這就是DCI要解決的根本問題。用戶認知一個個對象和它們所代表的領(lǐng)域,而每個對象還必須按照用戶心目中的交互模型去實現(xiàn)一些行為,通過它在用例中所扮演的角色與其他對象聯(lián)結(jié)在一起。正因為最終用戶能把兩種視角合為一體,類的對象除了支持所屬類的成員函數(shù),還可以執(zhí)行所扮演角色的成員函數(shù),就好像那些函數(shù)屬于對象本身一樣。換句話說,我們希望把角色的邏輯注入到對象,讓這些邏輯成為對象的一部分,而其地位卻絲毫不弱于對象初始化時從類所得到的方法。我們在編譯時就為對象安排好了扮演角色時可能需要的所有邏輯。如果我們再聰明一點,在運行時才知道了被分配的角色,然后注入剛好要用到的邏輯,也是可以做到的。
算法及角色-對象映射由Context擁有。Context“知道”在當前用例中應(yīng)該找哪個對象去充當實際的演員,然后負責把對象“cast”成場景中的相應(yīng)角色(cast 這個詞在戲劇界是選角的意思,此處的用詞至少符合該詞義,另一方面的用意是聯(lián)想到cast 在某些編程語言類型系統(tǒng)中的含義)。在典型的實現(xiàn)里,每個用例都有其對應(yīng)的一個Context 對象,而用例涉及到的每個角色在對應(yīng)的Context 里也都有一個標識符。Context 要做的只是將角色標識符與正確的對象綁定到一起。然后我們只要觸發(fā)Context里的“開場”角色,代碼就會運行下去。
于是我們有了完整的DCI架構(gòu)(Data、Context和Interactive三層架構(gòu)):
DCI目前廣泛被看作是對DDD的一種發(fā)展和補充,用在基于面向?qū)ο蟮念I(lǐng)域建模上。顯式的對role進行建模,解決了面向?qū)ο蠼V械某溲P秃拓氀P椭疇帯CI通過顯式的用role對行為進行建模,同時讓role在context中可以和對應(yīng)的領(lǐng)域?qū)ο筮M行綁定(cast),從而既解決了數(shù)據(jù)邊界和行為邊界不一致的問題,也解決了領(lǐng)域?qū)ο笾袛?shù)據(jù)和行為高內(nèi)聚低耦合的問題。
面向?qū)ο蠼C媾R的一個棘手問題是數(shù)據(jù)邊界和行為邊界往往不一致。遵循模塊化的思想,我們通過類將行為和其緊密耦合的數(shù)據(jù)封裝在一起。但是在復雜的業(yè)務(wù)場景下,行為往往跨越多個領(lǐng)域?qū)ο?#xff0c;這樣的行為如果放在某一個對象中必然會導致別的對象需要向該對象暴漏其內(nèi)部狀態(tài)。所以面向?qū)ο蟀l(fā)展的后來,領(lǐng)域建模出現(xiàn)兩種派別之爭,一種傾向于將跨越多個領(lǐng)域?qū)ο蟮男袨榻T陬I(lǐng)域服務(wù)中。如果這種做法使用過度,則會導致領(lǐng)域?qū)ο笞兂芍惶峁┮欢裧et方法的啞對象,這種建模結(jié)果被稱之為貧血模型。而另一派則堅定的認為方法應(yīng)該屬于領(lǐng)域?qū)ο?#xff0c;所以所有的業(yè)務(wù)行為仍然被放在領(lǐng)域?qū)ο笾?#xff0c;這樣導致領(lǐng)域?qū)ο箅S著支持的業(yè)務(wù)場景變多而變成上帝類,而且類內(nèi)部方法的抽象層次很難一致。另外由于行為邊界很難恰當,導致對象之間數(shù)據(jù)訪問關(guān)系也比較復雜,這種建模結(jié)果被稱之為充血模型。
關(guān)于多角色對象,舉個生活中的例子:
人有多重角色,不同的角色履行的職責不同:
這里人(大對象)聚合了多個角色(小類),人在某種場景下,只能扮演特定的角色:
引入DCI后,DDD四層架構(gòu)模式中的Domain層變薄了,以前Domain層對應(yīng)DCI中的三層,而現(xiàn)在:
因此,DDD分層架構(gòu)模式就變成了五層,如下圖所示:
ddd-l5.png
筆者在實踐中,將這五層的本地化定義為:
DDD五層架構(gòu)模式討論完了嗎?故事還沒有結(jié)束...
筆者參與的很多DDD落地實踐,都是面向控制面或管理面且消息交互比較多的系統(tǒng)。這類系統(tǒng)的一次業(yè)務(wù),包含一組同步消息或異步消息構(gòu)成的序列,如果都放在Context層,會導致該層的代碼比較復雜,于是我們考慮:
因此,在面向控制面或管理面且消息交互比較多的系統(tǒng)中,DDD分層架構(gòu)模式就變成了六層,如下圖所示:
ddd-l6.png
筆者在實踐中,將這六層的本地化定義為:
事務(wù)層的核心是事務(wù)模型,事務(wù)模型的框架代碼一般放在基礎(chǔ)設(shè)施層。關(guān)于事務(wù)模型,筆者以前分享過一篇文章—《Golang事務(wù)模型》,感興趣的同學可以看看。
綜上所述,DDD六層架構(gòu)可以看做是DDD五層架構(gòu)在特定領(lǐng)域的變體,我們統(tǒng)稱為DDD五層架構(gòu),而DDD五層架構(gòu)與傳統(tǒng)的四層架構(gòu)類似,都是限定型松散分層架構(gòu)。
模式三:六邊形架構(gòu)
有一種方法可以改進分層架構(gòu),即依賴倒置原則(Dependency Inversion Principle, DIP),它通過改變不同層之間的依賴關(guān)系達到改進目的。
依賴倒置原則由Robert C. Martin提出,正式定義為:
高層模塊不應(yīng)該依賴于底層模塊,兩者都應(yīng)該依賴于抽象。
抽象不應(yīng)該依賴于細節(jié),細節(jié)應(yīng)該依賴于抽象。
根據(jù)該定義,DDD分層架構(gòu)中的低層組件應(yīng)該依賴于高層組件提供的接口,即無論高層還是低層都依賴于抽象,整個分層架構(gòu)好像被推平了。如果我們把分層架構(gòu)推平,再向其中加入一些對稱性,就會出現(xiàn)一種具有對稱性特征的架構(gòu)風格,即六邊形架構(gòu)。六邊形架構(gòu)是Alistair Cockburn在2005年提出的,在這種架構(gòu)中,不同的客戶通過“平等”的方式與系統(tǒng)交互。需要新的客戶嗎?不是問題。只需要添加一個新的適配器將客戶輸入轉(zhuǎn)化成能被系統(tǒng)API所理解的參數(shù)就行。同時,對于每種特定的輸出,都有一個新建的適配器負責完成相應(yīng)的轉(zhuǎn)化功能。
六邊形架構(gòu)也稱為端口與適配器,如下圖所示:
ddd-hex.png
六邊形每條不同的邊代表了不同類型的端口,端口要么處理輸入,要么處理輸出。對于每種外界類型,都有一個適配器與之對應(yīng),外界通過應(yīng)用層API與內(nèi)部進行交互。上圖中有3個客戶請求均抵達相同的輸入端口(適配器A、B和C),另一個客戶請求使用了適配器D。假設(shè)前3個請求使用了HTTP協(xié)議(瀏覽器、REST和SOAP等),而后一個請求使用了AMQP協(xié)議(比如RabbitMQ)。端口并沒有明確的定義,它是一個非常靈活的概念。無論采用哪種方式對端口進行劃分,當客戶請求到達時,都應(yīng)該有相應(yīng)的適配器對輸入進行轉(zhuǎn)化,然后端口將調(diào)用應(yīng)用程序的某個操作或者向應(yīng)用程序發(fā)送一個事件,控制權(quán)由此交給內(nèi)部區(qū)域。
應(yīng)用程序通過公共API接收客戶請求,使用領(lǐng)域模型來處理請求。我們可以將DDD戰(zhàn)術(shù)設(shè)計的建模元素Repository的實現(xiàn)看作是持久化適配器,該適配器用于訪問先前存儲的聚合實例或者保存新的聚合實例。正如圖中的適配器E、F和G所展示的,我們可以通過不同的方式實現(xiàn)資源庫,比如關(guān)系型數(shù)據(jù)庫、基于文檔的存儲、分布式緩存或內(nèi)存存儲等。如果應(yīng)用程序向外界發(fā)送領(lǐng)域事件消息,我們將使用適配器H進行處理。該適配器處理消息輸出,而上面提到的處理AMQP消息的適配器則是處理消息輸入的,因此應(yīng)該使用不同的端口。
我們在實際的項目開發(fā)中,不同層的組件可以同時開發(fā)。當一個組件的功能明確后,就可以立即啟動開發(fā)。由于該組件的用戶有多個,并且這些用戶的側(cè)重點不同,所以需要提供多個不同的接口。同時,這些用戶的認識也是不斷深入的,可能會多次重構(gòu)相關(guān)的接口。于是,組件的多個用戶經(jīng)常會找組件的開發(fā)者討論這些問題,無形中降低了組件的開發(fā)效率。
我們換一種方式,組件的開發(fā)者在明確了組件的功能后就專注于功能的開發(fā),確保功能穩(wěn)定和高效。組件的用戶自己定義組件的接口(端口),然后基于接口寫測試,并不斷演進接口。在跨層集成測試時,由組件開發(fā)者或用戶再開發(fā)一個適配器就可以了。
六邊形架構(gòu)模式的演變
盡管六邊形架構(gòu)模式已經(jīng)很好,但是沒有最好只有更好,演變沒有盡頭。在六邊形架構(gòu)模式提出后的這些年,又依次衍生出三種六邊形架構(gòu)模式的變體,感興趣的讀者可以點擊鏈接自行學習:
小結(jié)
本文先和讀者一起回顧了DDD和分層架構(gòu)的相關(guān)知識,然后將DDD分層架構(gòu)中常用的三種模式(四層架構(gòu)、五層架構(gòu)和六邊形架構(gòu))結(jié)合實踐經(jīng)驗分別進行詳細闡述,使得讀者深刻理解DDD分層架構(gòu)模式,以便在微服務(wù)的開發(fā)實踐中根據(jù)具體情況選擇最合適的DDD分層架構(gòu)模式,從而交付結(jié)構(gòu)清晰且易維護的軟件產(chǎn)品。
?
總結(jié)
以上是生活随笔為你收集整理的还在搞三层架构?了解下 DDD 分层架构的三种模式吧的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: web网页设计实例作业 ——电影泰坦尼克
- 下一篇: 2022做跨境为什么要首选Lazada和