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