一文读懂DDD
2019獨角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
何為DDD
DDD不是架構(gòu)設(shè)計方法,不能把每個設(shè)計細(xì)節(jié)具象化,DDD是一套體系,決定了其開放性,體系中可以用任何一種方法來解決這些問題,但是如果一些關(guān)鍵問題沒有具體方案落地,可能讓團隊無所適從。
有的小伙伴覺得DDD太虛了,具體在我們進行業(yè)務(wù)代碼編寫落地中DDD主要解決什么問題呢?
總結(jié)起來說主要目的有兩點:
- 建立業(yè)務(wù)術(shù)語,統(tǒng)一PM/RD/QA需求溝通術(shù)語。
- 梳理業(yè)務(wù)邊界,將業(yè)務(wù)領(lǐng)域邏輯內(nèi)聚。
搞定DDD要解決的問題
- 如何進行領(lǐng)域建模
- 如何識別Bounded Context
- 如何在戰(zhàn)術(shù)層面尋找對象
DDD術(shù)語
戰(zhàn)略建模
- 界限上下文(Bounded Context)
- 上下文映射圖(Context Mapping)
戰(zhàn)術(shù)建模
- 聚合-Aggregate
- 實體-Entity
- 值對象-Value Objects
- 資源庫-Repository
- 領(lǐng)域服務(wù)-Domain Services
- 領(lǐng)域事件-Domain Events
- 模塊-Modules
Bound Context(BC)
領(lǐng)域中的BC被封裝為高內(nèi)聚的模塊,這種特性讓DDD對架構(gòu)并沒有太大侵入性。架構(gòu)可以應(yīng)用于領(lǐng)域內(nèi)部的結(jié)構(gòu),也可以包圍著領(lǐng)域模型,系統(tǒng)中可以采用多種風(fēng)格的架構(gòu)。
DDD的戰(zhàn)略設(shè)計上提出了BC(Bounded Context,界限上下文)。UL(Ubiquitous Language,通用語言)是團隊的共享語言,只要是團隊的一員,就需要使用UL,可以保證各個概念在各自上下文中無歧義。BC和UL是DDD的兩大支柱,相輔相成。
一個業(yè)務(wù)領(lǐng)域劃分成多個BC,BC之間通過Context Map進行集成,BC是一個顯示邊界,領(lǐng)域模型在這個邊界之內(nèi),領(lǐng)域模型是關(guān)于某個特定業(yè)務(wù)領(lǐng)域的軟件模型,領(lǐng)域模型通過對象模型來實現(xiàn),這些對象同時包含了數(shù)據(jù)和行為,并表達了準(zhǔn)確的業(yè)務(wù)含義。 廣義上講,領(lǐng)域是一個組織所做的事情及其中所包含的一切,表示整個業(yè)務(wù)系統(tǒng),領(lǐng)域表示應(yīng)該為整個業(yè)務(wù)系統(tǒng)創(chuàng)建統(tǒng)一的,內(nèi)聚的全功能模型,領(lǐng)域模型存在于BC內(nèi)。
通過BC隔離系統(tǒng)復(fù)雜性,將復(fù)雜度內(nèi)聚于邊界之內(nèi)。
一個大型系統(tǒng)的領(lǐng)域模型完全統(tǒng)一是不可行的,也不是一種經(jīng)濟有效的方式。任何一個大型項目都會存在多個模型,不同模型代碼組織在一起軟件可能會出現(xiàn)bug,同時更加不可靠并且難以理解。團隊之間溝通也會變的混亂。
當(dāng)劃分為多個模型之后,在模型之內(nèi),團隊可以自由工作,直到自己的界限并且恪守界限。所以需要確保模型純潔,一致和統(tǒng)一。 所以需要明確定義模型應(yīng)用上下文,根據(jù)團隊組織或者軟件系統(tǒng)或者物理表現(xiàn)來設(shè)置模型邊界。
Context Map 上下文圖
多個系統(tǒng)之間會發(fā)生關(guān)系,存在交互,需要在項目中創(chuàng)建一個所有模型上下文的全局視圖,減少混亂。一般通過Context Map表示系統(tǒng)關(guān)系總體視圖。
U表示上游(Upstream)的被依賴方,D表示下游(Downstream)的依賴方。防腐層(ACL)放在下游,將上游的消息轉(zhuǎn)化為下游的領(lǐng)域模型。
Context Map通過下面幾種方式表征界限上下文之間的關(guān)系:
- 共享內(nèi)核-Shared Kernel
- 客戶/供應(yīng)商-Customer/Supplier
- 追隨者-Conformist
- 防腐層-Anticorruption Layer
- 公開主機服務(wù)-Open Host Service
- 各行其道-Separate Way
共享內(nèi)核-Shared Kernel
當(dāng)不同團隊開發(fā)一些緊密相關(guān)的應(yīng)用程序時,團隊之間需要進行協(xié)調(diào),通常可以將兩個團隊共享的子集剝離出來形成共享內(nèi)核(Shared Kernel),雙方進行持續(xù)集成(Continuous Integration)。共享內(nèi)核(Shared Kernel)是業(yè)務(wù)領(lǐng)域中公共的部分,同時也是團隊間容易達成且必須達成共識的領(lǐng)域部分。
客戶/供應(yīng)商-Customer/Supplier
不同系統(tǒng)之間存在依賴關(guān)系時,下游系統(tǒng)依賴上游系統(tǒng),下游系統(tǒng)是客戶,上游系統(tǒng)是供應(yīng)商,雙方協(xié)定好需求,由上游系統(tǒng)完成模型的構(gòu)建和開發(fā),并交付給下游系統(tǒng)使用,之后進行聯(lián)調(diào)、測試。這種模式建立在團隊之間友好合作和支持的情況下。 當(dāng)兩個具有上游/下游關(guān)系的團隊不歸同一個管理者指揮時,Customer/Supplier這樣的合作模式就不會奏效。勉強應(yīng)用這種模式會給下游團隊帶來麻煩。
追隨者-Conformist
當(dāng)兩個開發(fā)團隊具有上/下游關(guān)系時,如果上游團隊沒有動機來滿足下游團隊的需求,那么下游團隊將無能為力。出于利他主義的考慮,上游開發(fā)人員可能會做出承諾,但他們可能不會履行承諾。下游團隊出于良好的意愿會相信這些承諾,從而根據(jù)一些永遠(yuǎn)不會實現(xiàn)的特性來制定計劃。下游項目只能被擱置.直到團隊最終學(xué)會利用現(xiàn)有條件自力更生為止。下游團隊不會得到根據(jù)他們的需求而量身定做的接口。 這時候“客戶/供應(yīng)商”模式就不湊效了,那么下游系統(tǒng)只能去追隨上游系統(tǒng),下游系統(tǒng)嚴(yán)格遵從上游系統(tǒng)的模型,簡化集成。 通過嚴(yán)格遵從上游團隊的模型,可以消除在 BC之間進行轉(zhuǎn)換的復(fù)雜性。盡管這會限制下游設(shè)計人員的風(fēng)格,而且可能不會得到理想的應(yīng)用程序模型,但選擇 Conformist模式可以極大地簡化集成。此外,這樣還可以與供應(yīng)商團隊共享一種 UL。供應(yīng)商處于駕駛者的位置上,因此最好使他們能夠容易溝通。
防腐層-Anticorruption Layer
前面介紹了在兩個BC之間集成時可以進行的各種合作,從高度合作的 Shared Kernel模式或 Customer/Supplier Team到單方面的Conformist模式。如果是一種更悲觀的關(guān)系,假設(shè)一個團隊既不可能與另一個團隊合作也無法利用他們的設(shè)計時,該如何應(yīng)對。 這時候我們需要使用防腐層(Anticorruption Layer)模式將上游系統(tǒng)的影響降低。
公開主機服務(wù)-Open Host Service
當(dāng)一個子系統(tǒng)必須與大量其他系統(tǒng)進行集成時,為每個集成都定制一個轉(zhuǎn)換層可能會減慢團隊的工作速度。如果一個子系統(tǒng)有某種內(nèi)聚性,那么或許可以把它描述為一組 Service,這組 Service滿足了其他子系統(tǒng)的公共需求。 公開主機服務(wù)(Open Host Service)能夠允許系統(tǒng)將一組Service公開出去公其他系統(tǒng)訪問。定義一個協(xié)議,把你的子系統(tǒng)作為一組 Service供其他系統(tǒng)訪問。開放這個協(xié)議,以便所有需要與你的子系統(tǒng)集成的人都可以使用它。當(dāng)有新的集成需求時,就增強并擴展這個協(xié)議,但個別團隊的特殊需求除外。
各行其道-Separate Way
當(dāng)兩個系統(tǒng)之間的關(guān)系并非必不可少時,兩者完全可以彼此獨立,各自獨立建模,獨立發(fā)展,互不影響。
領(lǐng)域事件
領(lǐng)域?qū)<宜P(guān)心的發(fā)生在領(lǐng)域中的一些事件。將領(lǐng)域中所發(fā)生的活動建模成一系列的離散事件。每個事件都用領(lǐng)域?qū)ο髞肀硎?..領(lǐng)域事件是領(lǐng)域模型的組成部分,表示領(lǐng)域中所發(fā)生的事情。
“重要的事件肯定會在系統(tǒng)其它地方引起反應(yīng),因此理解為什么會有這些反應(yīng)同樣也很重要。”
當(dāng)然領(lǐng)域事件并不是DDD所必須的。
一個領(lǐng)域事件可以理解為是發(fā)生在一個特定領(lǐng)域中的事件,是你希望在同一個領(lǐng)域中其他部分知道并產(chǎn)生后續(xù)動作的事件。但是并不是所有發(fā)生過的事情都可以成為領(lǐng)域事件。一個領(lǐng)域事件必須對業(yè)務(wù)有價值,有助于形成完整的業(yè)務(wù)閉環(huán),也即一個領(lǐng)域事件將導(dǎo)致進一步的業(yè)務(wù)操作。
領(lǐng)域事件可以是業(yè)務(wù)流程的一個步驟,例如訂單提交,客戶付費100元,訂單完工等。領(lǐng)域事件也可以是定時發(fā)生的事情,例如每晚對賬完成。或者是一個事件發(fā)生后引發(fā)的后續(xù)動作,例如客戶輸錯密碼三次后發(fā)生鎖定賬戶的事件。
領(lǐng)域事件也是一種基于事件的架構(gòu)(EDA)。事件架構(gòu)的好處可以把處理的流程解耦,實現(xiàn)系統(tǒng)可擴展性,提高主業(yè)務(wù)流程的內(nèi)聚性。
如果改為事件驅(qū)動模式,把訂單提交后觸發(fā)一個事件,在訂單保存后,觸發(fā)訂單提交事件。通知和后續(xù)的各種服務(wù)動作可以通過訂閱這個事件,在自己的實現(xiàn)空間內(nèi)實現(xiàn)對應(yīng)的邏輯,這樣就把訂單提交和后續(xù)其他非主要活動從訂單提交業(yè)務(wù)中剝離,實現(xiàn)了訂單提交業(yè)務(wù)高內(nèi)聚和低耦合性。
首先是解決領(lǐng)域的聚合性問題。DDD中的聚合有一個原則是,在單個事務(wù)中,只允許對一個聚合對象進行修改,由此產(chǎn)生的其他改變必須在單獨的事務(wù)中完成。如果一個業(yè)務(wù)跨多個聚合對象,領(lǐng)域事件會是一個不錯的工具來解決這個問題。通過領(lǐng)域事件的方式可以達到各個組件之間的數(shù)據(jù)一致性,通過最終一致性取代事務(wù)一致性。
其次領(lǐng)域事件也是一種領(lǐng)域分析的工具,有時從領(lǐng)域?qū)<业脑捴?#xff0c;我們看不出領(lǐng)域事件的跡象,但是業(yè)務(wù)需求依然有可能需要領(lǐng)域事件。動態(tài)流的事件模型加上結(jié)合DDD的聚合實體狀態(tài)和BC,可以有效進行領(lǐng)域建模。
領(lǐng)域事件可以通過觀察者模式和訂閱模式進行實現(xiàn)。比較常見的實現(xiàn)方式是事件總線(Event Bus)。
事件風(fēng)暴
事件風(fēng)暴是一項團隊活動,旨在通過領(lǐng)域事件識別出聚合根,進而劃分微服務(wù)的限界上下文。在活動中,團隊先通過頭腦風(fēng)暴的形式羅列出領(lǐng)域中所有的領(lǐng)域事件,整合之后形成最終的領(lǐng)域事件集合,然后對于每一個事件,標(biāo)注出導(dǎo)致該事件的命令(Command),再然后為每個事件標(biāo)注出命令發(fā)起方的角色,命令可以是用戶發(fā)起,也可以是第三方系統(tǒng)調(diào)用或者是定時器觸發(fā)等。最后對事件進行分類整理出聚合根以及限界上下文。
舉個例子
在我們的一次產(chǎn)品的重構(gòu)活動中也采用了事件風(fēng)暴方法。系統(tǒng)代碼維護了10幾年,代碼中存在大量的“壞味道”:重復(fù)代碼,過長函數(shù),過大的類,過長的參數(shù)列表,發(fā)散式變化,霰彈式修改,鍍金問題,注釋不清等問題。實際研發(fā)過程中也是經(jīng)常出現(xiàn)一點改動都可能會引起不可預(yù)測的結(jié)果,重構(gòu)勢在必行。 但是在重構(gòu)過程中,也沒有人可以說清楚現(xiàn)有系統(tǒng)的邏輯,如何重構(gòu)成為了一個難題。重構(gòu)過程我們引入了咨詢公司給我們的方法,采用了事件風(fēng)暴的辦法,通過對領(lǐng)域中所發(fā)生的事情(也就是領(lǐng)域事件)來探索這個領(lǐng)域,并且使用便簽來描述領(lǐng)域中的事件,這些便簽會沿著時間軸貼到一個很大的建模面板上。 舉例來說,能夠引發(fā)事件的事情包括用戶行為、外部系統(tǒng)所發(fā)生的事情以及時間的流逝。事件也有助于找到領(lǐng)域的邊界,對術(shù)語的不同闡述可能就意味著存在邊界。
-
準(zhǔn)備工作,四色貼紙: 橙色:事件,某個動作的結(jié)果,以“XX已XX”的方式表示,比如“用戶信息已查詢” 藍(lán)色:屬性,事件相關(guān)的輸入、輸出數(shù)據(jù)等 黃色:命令,某個動作,比如“查找用戶信息” 綠色:實體,命令的觸發(fā)者
-
開始梳理業(yè)務(wù),將結(jié)果貼到白版上
-
繼續(xù)深入梳理,將整個過程的模型、關(guān)鍵數(shù)據(jù)等梳理出來,貼在白板上
-
確定重構(gòu)指導(dǎo)思路,執(zhí)行重構(gòu)動作,重構(gòu)的同時引入單元測試保障重構(gòu)的質(zhì)量
實體和值對象
實體不僅需要知道它是什么?而且還需要知道它是哪個?而值對象只需要知道它是什么?
-
實體:許多對象不是由它們的屬性來定義,而是通過一系列的連續(xù)性(continuity)和標(biāo)識(identity)來從根本上定義的。只要一個對象在生命周期中能夠保持連續(xù)性,并且獨立于它的屬性(即使這些屬性對系統(tǒng)用戶非常重要),那它就是一個實體。
-
值對象:當(dāng)你只關(guān)心某個對象的屬性時,該對象便可作為一個值對象。為其添加有意義的屬性,并賦予它相應(yīng)的行為。我們需要將值對象看成不變對象,不要給它任何身份標(biāo)識,還應(yīng)該盡量避免像實體對象一樣的復(fù)雜性。
實體對象相對容易理解,我們常見的類的都可以看成是實體對象。值對象在DDD中相對而言是難以理解并且容易誤用的。
為什么需要使用值對象,書中給了一個解釋:
使用不變的值對象使得我們做更少的職責(zé)假設(shè)
使用值對象在不同的BC中進行數(shù)據(jù)交換,可以避免不同BC對實體對象的狀態(tài)變更而引發(fā)的數(shù)據(jù)依賴關(guān)系,實現(xiàn)最小化的集成。
值類型用于度量和描述事物,DDD中建議應(yīng)盡量使用值對象來建模而不是實體對象,因為值對象非常容易地對值對象進行創(chuàng)建、測試、使用、優(yōu)化和維護。
領(lǐng)域服務(wù)
領(lǐng)域中的服務(wù)表示一個無狀態(tài)的操作,它用于實現(xiàn)特定于某個領(lǐng)域的任務(wù)。 當(dāng)某個揉作不適合放在聚合和值對象上時,最好的方式便是使用領(lǐng)域服務(wù)了。有時我們傾向于使用聚合根上的靜態(tài)方法來實現(xiàn)這些這些操作,但是在 DDD中,這是一種壞味道。
《實現(xiàn)領(lǐng)域驅(qū)動設(shè)計》書中給出了一個例子,對User進行認(rèn)證的例子。例子中給出的需求是:
- 系統(tǒng)必須對User進行認(rèn)證,并且只有當(dāng)Tenant處于激活狀態(tài)時候才能對User進行認(rèn)證。
- 必須對密碼進行加密,并且不能使用明文密碼
對以上的需求,我們可以把認(rèn)證的方法寫在User類或者Tenant類中,不過對于以上解決方案,似乎都給模型帶來了太多的問題。
對于后一種方案, 我們必須從以下回種解決辦法中選擇一種:
在Tenant中處理對密碼的加密,然后將加密后的密碼傳給User。這種方法違背了單一職責(zé)原則
由于一個User必須保征對密碼的加密,它可能已經(jīng)知道了一些加密信息。如果是這樣,我們可以在User上創(chuàng)建一個方法,該方法對明文密碼進行認(rèn)證。但是在這種方式下,認(rèn)證過程變成了Tenant上的Facade。而實際的認(rèn)證 功能全在User上。另外User上的認(rèn)證方法必須聲明為Protected,以防止外界 客戶端對認(rèn)證方法的直接調(diào)用。
Tenant依賴于User對密碼進行加密,然后將加密后的密碼與原有密碼進行匹配。這種方法似乎在對象協(xié)作之間增加了額外的步驟。此時,Tenant依然需 要知道認(rèn)證細(xì)節(jié)。
讓客戶端對密碼進行加密。然后將其傳給Tenant,這樣導(dǎo)致的問題在于客戶端承載了它本不應(yīng)該有的職責(zé)。
模塊
在DDD中,模塊表示了一個命名的容器,用于存放領(lǐng)域中內(nèi)聚在一起的類。
模塊應(yīng)該包含一組具有高內(nèi)聚性的概念集合.這樣做的好處是可以在不同的模塊之間實現(xiàn)松耦合。否則,我們應(yīng)該修改模型以重新劃分這些概念。……由于模塊名是UL的一部分,模塊名應(yīng)該反映出它們在領(lǐng)域中的概念。[Evans]
模塊的設(shè)計是基于領(lǐng)域模型的,要符合通用語言的表述。其次,模塊的設(shè)計要符合高內(nèi)聚低耦合的設(shè)計思想。
模塊和BC的關(guān)系
模塊與子域和限界上下文并不是一致的概念,模塊也是一種獨立的建模方法。對于何時應(yīng)該對領(lǐng)域模型進行分離,何時將領(lǐng)域模型建模成一個整體,應(yīng)該仔細(xì)地思考與對待。有時通用語言可以很好地幫助我們做出正確的選擇。但是另外的時候,其中的術(shù)語將變得非常含糊。在這種情況下,我們并不清楚如何劃分上下文邊界。此時,我們可以首先將它們放在一起,使用模塊來對模型進行劃分,面不是限界上下文。
但是,這并不意味著我們就應(yīng)該限制對限界上下文的創(chuàng)建。我們應(yīng)該通過通用語言的需求來劃分模型邊界。但限界上下文不是用來代替模塊的。使用摸塊的目的在于組織那些內(nèi)聚在一起的領(lǐng)域?qū)ο?#xff0c;對于那些內(nèi)聚性不強或者沒有內(nèi)聚性的領(lǐng)域?qū)ο髞碚f,我們應(yīng)該將它們劃分在不同的模塊中。
集成BC(界限上下文)
一個項目中會存在多個BC,業(yè)務(wù)需要對它們進行集成。有多種直接的方法進行集成。最簡單的方式就是一個BC中暴露API,然后在另外一個BC中通過RPC進行調(diào)用。
另外我們也可以通過消息機制進行集成,系統(tǒng)通過消息隊列或者發(fā)布-訂閱機制進行通訊。
第三種方式是通過使用RESTful的方式進行集成。當(dāng)然,還存在有其他的集成方式。
結(jié)尾一張圖
如果你還是云里霧里,參考這張圖:
歡迎加微信交流:
轉(zhuǎn)載于:https://my.oschina.net/u/1000241/blog/3049514
總結(jié)
- 上一篇: Python scrapy爬取京东,百度
- 下一篇: Excel关于使用图形的妙招技法