一文讀懂DDD

何爲DDD

DDD不是架構設計方法,不能把每一個設計細節具象化,DDD是一套體系,決定了其開放性,體系中能夠用任何一種方法來解決這些問題,可是若是一些關鍵問題沒有具體方案落地,可能讓團隊無所適從。微信

有的小夥伴以爲DDD太虛了,具體在咱們進行業務代碼編寫落地中DDD主要解決什麼問題呢?架構

總結起來講主要目的有兩點:app

  • 創建業務術語,統一PM/RD/QA需求溝通術語。
  • 梳理業務邊界,將業務領域邏輯內聚。

搞定DDD要解決的問題

  • 如何進行領域建模
  • 如何識別Bounded Context
  • 如何在戰術層面尋找對象

DDD術語

戰略建模ide

  • 界限上下文(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)。事件架構的好處能夠把處理的流程解耦,實現系統可擴展性,提升主業務流程的內聚性。

若是改成事件驅動模式,把訂單提交後觸發一個事件,在訂單保存後,觸發訂單提交事件。通知和後續的各類服務動做能夠經過訂閱這個事件,在本身的實現空間內實現對應的邏輯,這樣就把訂單提交和後續其餘非主要活動從訂單提交業務中剝離,實現了訂單提交業務高內聚和低耦合性。

  1. 首先是解決領域的聚合性問題。DDD中的聚合有一個原則是,在單個事務中,只容許對一個聚合對象進行修改,由此產生的其餘改變必須在單獨的事務中完成。若是一個業務跨多個聚合對象,領域事件會是一個不錯的工具來解決這個問題。經過領域事件的方式能夠達到各個組件之間的數據一致性,經過最終一致性取代事務一致性。

  2. 其次領域事件也是一種領域分析的工具,有時從領域專家的話中,咱們看不出領域事件的跡象,可是業務需求依然有可能須要領域事件。動態流的事件模型加上結合DDD的聚合實體狀態和BC,能夠有效進行領域建模。

領域事件能夠經過觀察者模式和訂閱模式進行實現。比較常見的實現方式是事件總線(Event Bus)。

事件風暴

事件風暴是一項團隊活動,旨在經過領域事件識別出聚合根,進而劃分微服務的限界上下文。在活動中,團隊先經過頭腦風暴的形式羅列出領域中全部的領域事件,整合以後造成最終的領域事件集合,而後對於每個事件,標註出致使該事件的命令(Command),再而後爲每一個事件標註出命令發起方的角色,命令能夠是用戶發起,也能夠是第三方系統調用或者是定時器觸發等。最後對事件進行分類整理出聚合根以及限界上下文。

舉個例子

在咱們的一次產品的重構活動中也採用了事件風暴方法。系統代碼維護了10幾年,代碼中存在大量的「壞味道」:重複代碼,過長函數,過大的類,過長的參數列表,發散式變化,霰彈式修改,鍍金問題,註釋不清等問題。實際研發過程當中也是常常出現一點改動均可能會引發不可預測的結果,重構勢在必行。
可是在重構過程當中,也沒有人能夠說清楚現有系統的邏輯,如何重構成爲了一個難題。重構過程咱們引入了諮詢公司給咱們的方法,採用了事件風暴的辦法,經過對領域中所發生的事情(也就是領域事件)來探索這個領域,而且使用便籤來描述領域中的事件,這些便籤會沿着時間軸貼到一個很大的建模面板上。
舉例來講,可以引起事件的事情包括用戶行爲、外部系統所發生的事情以及時間的流逝。事件也有助於找到領域的邊界,對術語的不一樣闡述可能就意味着存在邊界。

  • 準備工做,四色貼紙:
    橙色:事件,某個動做的結果,以「XX已XX」的方式表示,好比「用戶信息已查詢」
    藍色:屬性,事件相關的輸入、輸出數據等
    黃色:命令,某個動做,好比「查找用戶信息」
    綠色:實體,命令的觸發者

  • 開始梳理業務,將結果貼到白版上
  • 繼續深刻梳理,將整個過程的模型、關鍵數據等梳理出來,貼在白板上
  • 肯定重構指導思路,執行重構動做,重構的同時引入單元測試保障重構的質量

實體和值對象

實體不只須要知道它是什麼?並且還須要知道它是哪一個?而值對象只須要知道它是什麼?

  • 實體:許多對象不是由它們的屬性來定義,而是經過一系列的連續性(continuity)和標識(identity)來從根本上定義的。只要一個對象在生命週期中可以保持連續性,而且獨立於它的屬性(即便這些屬性對系統用戶很是重要),那它就是一個實體。

  • 值對象:當你只關心某個對象的屬性時,該對象即可做爲一個值對象。爲其添加有意義的屬性,並賦予它相應的行爲。咱們須要將值對象當作不變對象,不要給它任何身份標識,還應該儘可能避免像實體對象同樣的複雜性。

實體對象相對容易理解,咱們常見的類的均可以當作是實體對象。值對象在DDD中相對而言是難以理解而且容易誤用的。

爲何須要使用值對象,書中給了一個解釋:

使用不變的值對象使得咱們作更少的職責假設

使用值對象在不一樣的BC中進行數據交換,能夠避免不一樣BC對實體對象的狀態變動而引起的數據依賴關係,實現最小化的集成。

值類型用於度量和描述事物,DDD中建議應儘可能使用值對象來建模而不是實體對象,由於值對象很是容易地對值對象進行建立、測試、使用、優化和維護。

領域服務

領域中的服務表示一個無狀態的操做,它用於實現特定於某個領域的任務。
當某個揉做不適合放在聚合和值對象上時,最好的方式即是使用領域服務了。有時咱們傾向於使用聚合根上的靜態方法來實現這些這些操做,可是在 DDD中,這是一種壞味道。

《實現領域驅動設計》書中給出了一個例子,對User進行認證的例子。例子中給出的需求是:

  • 系統必須對User進行認證,而且只有當Tenant處於激活狀態時候才能對User進行認證。
  • 必須對密碼進行加密,而且不能使用明文密碼

對以上的需求,咱們能夠把認證的方法寫在User類或者Tenant類中,不過對於以上解決方案,彷佛都給模型帶來了太多的問題。

對於後一種方案, 咱們必須從如下回種解決辦法中選擇一種:

  1. 在Tenant中處理對密碼的加密,而後將加密後的密碼傳給User。這種方法違背了單一職責原則

  2. 因爲一個User必須保徵對密碼的加密,它可能已經知道了一些加密信息。若是是這樣,咱們能夠在User上建立一個方法,該方法對明文密碼進行認證。可是在這種方式下,認證過程變成了Tenant上的Facade。而實際的認證 功能全在User上。另外User上的認證方法必須聲明爲Protected,以防止外界 客戶端對認證方法的直接調用。
  3. Tenant依賴於User對密碼進行加密,而後將加密後的密碼與原有密碼進行匹配。這種方法彷佛在對象協做之間增長了額外的步驟。此時,Tenant依然需 要知道認證細節。
  4. 讓客戶端對密碼進行加密。而後將其傳給Tenant,這樣致使的問題在於客戶端承載了它本不該該有的職責。

UserDescriptor userDescriptor = 
          DomainRegistry
            .authenticationService()
            .authenticate(tenantID,userName,password);

模塊

在DDD中,模塊表示了一個命名的容器,用於存放領域中內聚在一塊兒的類。

模塊應該包含一組具備高內聚性的概念集合.這樣作的好處是能夠在不一樣的模塊之間實現鬆耦合。不然,咱們應該修改模型以從新劃分這些概念。……因爲模塊名是UL的一部分,模塊名應該反映出它們在領域中的概念。[Evans]

模塊的設計是基於領域模型的,要符合通用語言的表述。其次,模塊的設計要符合高內聚低耦合的設計思想。

模塊和BC的關係

模塊與子域和限界上下文並非一致的概念,模塊也是一種獨立的建模方法。對於什麼時候應該對領域模型進行分離,什麼時候將領域模型建模成一個總體,應該仔細地思考與對待。有時通用語言能夠很好地幫助咱們作出正確的選擇。可是另外的時候,其中的術語將變得很是含糊。在這種狀況下,咱們並不清楚如何劃分上下文邊界。此時,咱們能夠首先將它們放在一塊兒,使用模塊來對模型進行劃分,面不是限界上下文。

可是,這並不意味着咱們就應該限制對限界上下文的建立。咱們應該經過通用語言的需求來劃分模型邊界。但限界上下文不是用來代替模塊的。使用摸塊的目的在於組織那些內聚在一塊兒的領域對象,對於那些內聚性不強或者沒有內聚性的領域對象來講,咱們應該將它們劃分在不一樣的模塊中。

集成BC(界限上下文)

一個項目中會存在多個BC,業務須要對它們進行集成。有多種直接的方法進行集成。最簡單的方式就是一個BC中暴露API,而後在另一個BC中經過RPC進行調用。

另外咱們也能夠經過消息機制進行集成,系統經過消息隊列或者發佈-訂閱機制進行通信。

第三種方式是經過使用RESTful的方式進行集成。固然,還存在有其餘的集成方式。

結尾一張圖

若是你仍是雲裏霧裏,參考這張圖:

歡迎加微信交流:

相關文章
相關標籤/搜索