1. 程式人生 > >領域驅動設計整理——概念&架構

領域驅動設計整理——概念&架構

領域、子域、限界上下文

DDD(Domain-Drive Design)的概念或者說業界的聲音其實可以追溯到幾十年前了。最近開始想要系統得整理一下DDD的一些東西。這一篇是一個簡單的引子,也是mark一下自己接觸到的概念和理解。
對於領域的概念其實很好理解,就如字面意思一樣,比如出版書籍領域,廣告設計領域。圈定了一定的範疇,並且在這個範疇內,所有團隊成員對於某一概念的理解是一致的。比如書籍就是我們需要出版的書籍,而不是比如出版社給員工提供的什麼季度獎勵書籍。也許這裡的例子還是有點偏差,但是當具體設計系統的時候就會去思考真正的業務邊界。
我理解的領域驅動設計不是一種設計風格,也不是一種架構模型。而是一種思考方式。在考慮一個專案、需求的時候不會先去思考需要怎樣的資料儲存結構以及會有哪些行為和操作需要實現。而是去思考業務中一些重要的核心界限以及概念。最重要的就是定義通用語言。如何定義一個限界上下文的通用語言,來保證在具體的系統設計中,也即一個限界上下文中是一個唯一概念是非常重要的。限界上下文包含了模組(領域模型)以及上下文,所以它也是一個顯示邊界。
限界上下文中的術語一定準確反映通用語言

限界上下文可以是我們的一個系統。比如最初,我們要給一個圖書館客戶做一個系統,包含了圖書館的書籍管理,入庫、借出。然後圖書館又提出,比如某些書籍是專門賣的,然後某些書籍是專門買進來獎勵一些會員的獎品。然後我們需要確定,“書”這個概念,是不是已經引起了歧義。當我們的開發人員說出書這個詞的時候,可能有的人理解是需要借的書,有的人理解是回饋客戶的獎品。這個時候就出現了對於“書”的理解不一致,也就違反了領域模型的通用性。我們的系統邊界,應該是完整的表達一個領域模型,或者說是通用語言。這就像音樂家創作的交響樂譜一樣,多一個音符不多,少一個音符不少,剛剛好好,完整而優美得可以演奏出來一篇華麗的作品。所以系統設計也是一樣,我們所說的領域專家其實就是具備能細緻得劃分領域邊界,以及從複雜的業務中抽象出來通用語言的人。所以剛才的圖書館的例子,在落地到系統中的時候,就需要考慮系統劃分了。出售書的系統中的書,不會使用租書系統中的書的實體來處理自己的領域事件。
在限界上下文定義好之後,就可以劃定核心域了。比如這裡,圖書館的核心業務是租書來收取定期的閱讀費用。那麼租書上下文中的租書服務就是我們的核心域了。圖書館的場景中,還需要有一個獨立的使用者系統,來管理使用者登入和使用者基本資訊。租書上下文,售書上下文都會需要呼叫使用者管理上下文。使用者管理上下文就是一個通用子域。當然還有其他子域,有些子域可能共享一些狀態,是一種協作上下文。

上下文對映

當我們需要設計多個上下文來完成需求的時候,就會要考慮上下文之間是否存在著一些聯絡。就如上述,使用者管理上下文就是一個上游系統,成為其他系統的支撐子域。當我們需要在租書系統中需要使用者資訊的時候可以有幾種做法:

  1. 共享核心方式:對於模型和程式碼的共享,但是這需要團隊之間協作緊密,始終保持通用語言的一致性
  2. 客戶-供應關係:上游團隊的開發需獨立於下游,並且也要儘量估計下游團隊是需求和時間。
  3. 防腐層(Anticorruption Layer): 下游系統對上游模型進行翻譯。防腐層的介面用來和其他系統互動,在防腐層內部,來實現模型概念的轉換。
  4. 開放主機服務(Open Host Service):定義協議,讓其他系統通過協議訪問系統服務。
  5. 釋出語言(Published Language):在兩個限界上下文之間釋出一個公用語言應用於翻譯模型中。

在實際的整合中我們可呢過需要結合多種對映方式。比如,如果使用分離核心(和共享核心相對的),可以在上游使用者管理上下文中使用開放主機服務和釋出語言,然後下游的借書上下文通過防腐層來進行模型的轉換。這種上下文的對映保證了通用語言在所有模組的純潔性。每個上下文的開發團隊就只需要專注於自己上下文的領域和統一的語言環境中就可以了。
在實際的實現手段上可以有多種方式,比如開放主機服務,可以使用Rest的方式,或者RPC 機制,或者訊息機制。釋出語言可以使用XML、Json、protocal buffer等或者訊息的形式。防腐層就是把外界的概念翻譯成本上下文的物件。在開發過程中也可以用一些概念對映關係來表達通用語言。團隊成員共享這些概念,並且時刻提醒自己所關注的領域範圍。上游上下文需要把資源不可用顯示暴露出來,下游上下文需要能正確處理上游的模型狀態。在實施中可以用訊息機制,或者做一些非同步的探測依賴的上下文的可用狀態等。

DDD架構

終於到了正文。後面按照幾種比較普遍的分類陳述。

依賴倒置

現在主流的框架中,依賴倒置簡言之就是依賴介面、抽象,而不是依賴具體的實現。這種分層的形式,在現在的系統架構中都普遍流行。Java語言的Spring就是一個典型的踐行依賴倒置的框架。這種基於抽象的分層,能讓應用服務和領域服務很好的解耦。但是有時候過度得設計分層也會倒置貧血模型。貧血模型和反模式這種話題也是若干年沒有討論出一個誰是誰非的話題,這裡就不展開了。其實,只要是對領域模型有正確的抽象,能反應出一套統一的通用語言,再根據具體的業務時間去選擇自己的架構方式就足夠了。
所以這裡希望在實踐的過程中,需要堅持不要過度依賴應用服務,讓應用服務的抽象承擔了太多的領域服務職責就可以了。另外,應用服務和領域服務是兩個完全不同的概念。應用服務可以和其他上下文進行服務輸出、輸入、可以處理事務和複雜的業務邏輯。但是領域服務是更加輕量級的,為應用服務提供領域操作的。如果將計算和驗證結合在實體中就是充血模式,如果是需要多個領域聚合的就可以再抽象出來一層單獨的領域服務層。當然領域服務也不一定要用依賴倒置,或者說,業務領域服務根本不需要介面,因為領域專屬的東西,不希望向客戶端洩露擴充套件方式。

六邊形

六邊形的架構是很適合和領域驅動結合的架構方式。六邊形架構的簡單架構圖如下:六邊形架構
圖片來自《實踐領域驅動設計》。無論是SOA 或者是RESTful 都很適用六邊形的架構。我覺得這裡的六邊形體現了一種平等性,就像蜂巢一樣。每一個窩都是平等的,並且可以很好地互動。新增一個擴充套件的Client只要新增一個介面卡就可以將client的輸入轉化成系統內部的API的引數模型。系統輸出服務也是一樣的。所以六便習慣的關鍵就是每個介面卡。每個外界的型別都有一個介面卡想對應。外界通過API和系統互動,可以使一個Http請求,也可以是訊息機制。介面卡將外界的請求或者內部的輸出都通過API引數的形式來設計和互動。對於引數對映可以採用很多框架來幫助完成。六邊形的內部就是領域模型的應用。通過介面卡,能進行很好的系統間防腐,做到外界引數和領域模型的互相轉換。Spring 框架對於Restful的引數對映和資源對映也有很好的支援。通過註解的方式,就可以完成介面卡應該做的工作。具體的例子可以參看下Spring Annotation Based Controllers

SOA

SOA 架構圖如下:
SOA六邊形 SOA結構中,服務的邊界是在六邊形的外層(這裡的六邊形依然是描述了單個上下文的結構)。在設計SOA 架構的時候,不應該以REST 或者SOAP 或訊息型別來決定上下文的大小。這樣會導致多個小的限界上下文和領域模型。所以在設計服務導向的上下文的時候需要注意保證上下文所要表達的通用語言的領域模型。

RESTFul Http

RESTFul 的架構中,每個資源都有一個URI,通過資源的方式來向外界提供操作和訪問入口。Restful是具有”Presentation”和”State”的。
- 展現的格式可以是xml、json或者html,或者二進位制的資料。
- 無狀態表示的是一種請求的獨立性,提高可上下文的可伸縮性。
對於資源確定之後就可以確定操作介面(如Http的get、post、put、delete等)。但是在設計介面中不應該將領域模型暴露給外界,不能因為領域模型的改變導致系統介面的變化。所以Restful 可以結合六邊形架構。用Spring的web.bind服務就可以很好地實現這個上下文邊界的設計。
為了和領域驅動很好的結合,還可以和微服的架構思想一起考慮,如果每個上下文都是一個微服,可以獨立部署。那麼在每個領域的限界上下文之間可以建立一個統一的系統介面層。所有需要多個領域上下文的請求都可以通過這個系統介面層。將核心領域和各個協作上下文的領域解耦。這種是對於各個上下文沒有互相依賴的情況下來說是一種很好的服務輸出方式,並且還可以用很多非同步、併發控制框架來提高請求處理的效能。
對於上下文之間依賴,其實除了RPC,也可以通過事件驅動或者還是用Restful的方式進行整合。但是無論以什麼方式或者技術手段來實現,都要注意不要將外部的領域模型暴露給本地系統,或者把本地限界上下文的領域模型暴露給外部。具體的處理方式,依據不同的架構會有不同的實現。如果是Restful 的方式,可以通過一個介面卡(Adapter)來實現http 請求的轉發,和對本地模型(DTO)對映的處理。然後將模型轉化進一步交給翻譯器(Translator),翻譯器負責將遠端物件(其他限界上下文的DTO,而非領域模型)轉化為本地DTO,這個過程可以用Spring的RestTemplate(也可以進一步對於template封裝一層本地服務的Facade)來幫助對HttpClient 的封裝和JSON資料進行處理。

CQRS

CQRS 是命令查詢的責任分離Command Query Responsibility Segregation的簡稱。其實可以簡單理解為讀(Query 請求)寫(Commend請求)服務的分離。CQRS可以很好的解決複雜的介面顯示的問題。一個介面或者是獲取引數處理命令的,或者是返回資料的。這樣的分離可以在讀寫兩個層次上分別抽取出不同的系統服務。C和Q的資料可以通過領域事件的方式進行同步。CQRS一篇很好的總結可以參考一下CQRS架構簡介

事件驅動

Event-Driven Architecture(EDA) 通過訊息機制可以很好地完成上下文之間的解耦。當然在選擇用訊息整合的時候,對於可靠性和實時性上的要求需要做好權衡。事件驅動可以結合在六邊形的架構和Restful架構中。作為一種輔助的解耦方式。本地的訊息可以通過guava的Eventbus,叢集的可以通過RabbitMQ來實現。
對於事件源來說,事件釋出需要對於領域物件的修改進行跟蹤,聚合上的每一次操作都有一個領域事件釋出出去,每一個領域事件都需要被儲存到一個時間儲存中,這樣可以保證對於事件釋出前後的各個狀態都能回溯。在實現最終一致性上,訊息機制往往需要更加複雜的處理。訂閱方也即Observer中的觀察者需要對時間進行儲存。當客戶端需要查詢聚合例項的時候,通過資源庫再向事件儲存中查詢最終需要的聚合例項。當聚合例項發生變化的時候再執行實踐的釋出。圍繞一個聚合的例項就形成了一個生產和消費的閉環。整個事件機制中還要考慮重發機制以及超時時間。訂閱方需要冪等得處理事件,並且再狀態不一致的時候,可以進行失敗補償。如果允許失敗,那就可以直接採用工作流的方式。在具體的實施中可以有很多方式,都需要根據實際的場景和投入產出做具體的衡量。

資料網織

DataFabric 主要是在大資料處理上的一種架構方案,處理DB 效能瓶頸的時候可以將領域模型以序列化的方式放到快取中,具體的儲存方式可以是文字、json格式,也可以是二進位制資料。可以通過很多Nosql技術來實現,GemFire、Coherence、redis、Mongo等。

結語

以上的所有架構都可以通過DDD的思想進行實現,在實際的系統架構上,肯定也不止是隻應用其中的一種。六邊形一般都可以結合Restful或者事件驅動。當然無論採用什麼架構或者技術手段,迴歸到領域驅動的核心就是對於通用語言的定義。限界上下文要保證自己的純粹性,儘量減少上下文間的遵奉關係或者共享核心。儘量保證系統的自治性,對其他系統的無感知性,不要將系統內部的領域模型暴露給客戶端。後面會繼續抽空對DDD的實施進行詳細的總結和整理。