實現領域驅動設計之感悟(一)


接觸領域驅動設計的概念,已有4年了。從看書了解的純理論,到實際項目應用中遇到建模問題的思考,逐漸提升了建模能力。正好碰到2020年五一放假,想趁這個機會,寫一下我的學習感悟。

什么情況下需要引入領域驅動設計

公司內的業務沉淀達到一定量,現有老系統維護困難,這個時候,有必要引入領域驅動設計,在這里簡稱DDD。

產品經理的業務設計和最終產品實現出入比較大,往往功能看似一樣,實質在業務變更時會難以實現。解決辦法是讓產品經理參與進來,在軟件建模期間和開發人員保持一致,以降低最終實現和業務設計的差異。

后續會分別從了解領域的概念、領域建模,到實戰開發,逐步講解清楚。

領域(Domain)

我理解為是所處的行業,譬如電商行業、制造業、運輸業,等等等等。也可以是細分的,如零售行業、汽車制造行業。那么這些,可以稱之為領域。

解決方案空間(Solution Space)、問題空間(Problem Space)

在特定領域下,通過產品經理和業務人員的共同努力,針對面臨的業務問題,進行解決。已解決的部分,就是解決方案空間(需求);尚未解決的,就是問題空間(潛在需求)。

對於領域專家(產品經理)的職責:

  • 1)和業務人員一起,針對公司業務戰略方向,符合公司當前、乃至未來發展方向決策,制定具體業務;或者針對現有面臨的業務問題,進行業務調整;

  • 2)就已制定的業務,對開發人員進行初步業務講解;

  • 3)開發人員建模期間幫助開發人員深入了解業務;對於開發人員提出確實不合理或權衡下來很難實現或開發成本高的地方,協商調整,從而確保最終出來的軟件,能符合制定業務的初衷。

對於開發人員的職責:

  • 1)和領域專家參與業務討論,並對領域進行建模;

  • 2)對於不合理的,或有更好提議,可以提出來,幫助領域專家,完善業務;

  • 3)按領域建模,進行業務開發;

  • 4)非功能性需求實現:穩定性、可用性、可伸縮性。

子域(Subdomain)、通用子域(Generic Subdomain)、支撐子域(Supporting Subdomain)、核心域(Core Domain)

子域:在解決方案空間中,將領域細分成各個子業務,每個子業務稱之為子域。

通用子域:對於和業務關聯性不大的子域,具有可替代性的,稱之為通用子域。如權限管理系統,初期可以通過購買產品以節省人力成本。

支撐子域:子域之間,存在一個依賴關系,作為當前子域的上游子域,稱之為支撐子域。如產品目錄子域,是訂單子域的支撐子域。

核心域:對於公司,具有核心競爭力的業務,對應的子域,即為核心域。公司發展的不同時期,核心域也會隨之改變。(原來的核心域,可能會變成新的核心域的支撐子域)

限界上下文(Bounded Context)

限界上下文:一個業務系統,稱之為限界上下文。如erp系統,就是個限界上下文。老的erp系統改造前,會涵蓋所有子域。經過按子域進行剝離后,被剝離的子域,將擁有獨立的限界上下文。如erp里的訂單模塊、商品管理模塊、倉庫管理模塊。被剝離后,變成訂單系統、商品中心、倉庫管理系統。

通用語言(Ubiquitous Language)

通用語言:在當前限界上下文,建模時溝通用的語言,稱為通用語言,是開發人員與領域專家(產品經理就是)之間的橋梁。如同大家坐在一起討論特定主題時,都是圍繞主題發言,涉及的名詞也是在這個語境下進行,不會產生理解分歧問題。

通用語言中使用的名詞,一般由行業專業術語 + 公司特有業務術語組成;僅作用於當前的限界上下文,具有無歧義性的特點;用於描述業務對象屬性及行為、業務場景。

限界上下文映射(Bounded Context Map)

限界上下文映射:多個限界上下文之間存在交互,為了直觀表達之間的關系,我們需要一張系統交互關系圖,來表達上下游之間的關系,這就是限界上下文映射。

我將分別按組織模式、集成模式,進行討論。此處的限界上下文的集成,我們可以簡稱為系統集成。

組織模式

組織模式,即團隊間的關系。上下游團隊配合度高的組織,就當前限界上下文而言,自然集成的代價是最小的。

  • 合作關系(Partnership):團隊之間,榮辱與共,利益關系為一個整體。這種團隊關系,是最容易做系統集成的。

  • 共享內核(Shared Kernel):將模型和代碼進行共享,用於團隊之間。優點是通過代碼集成,集成相對容易;缺點是一旦改動,需要相關的團隊都要參與討論確定后才可以修改,耦合度比較高。

  • 客戶方-供應方開發(Customer-Supplier Development):上游團隊獨立於下游團隊完成開發,此時下游團隊的開發會受限制。解決辦法是在上游團隊開發期間,有預見性的將下游開發的依賴需求考慮在內,並列入開發計划。

  • 遵奉者(Conformist):上游團隊無動力去為滿足下游團隊進行的維護開發工作。即便處於利他主義考慮,而作出了種種承諾,可能難以一一兌現,導致下游團隊開發處於被動狀態。

集成模式

  • 防腐層(Anticorruption Layer):在當前領域模型中,封裝對上游系統使用的一個翻譯層,當前系統通過這個翻譯層與上游系統交互。當組織模式可以順利通過 合作關系共享內核客戶方-供應方開發 進行合作開發的時候,所做的翻譯層將會很輕松;否則,會比較艱難。防腐層,還可以通過引入必要的數據同步,解決因上游系統的不可用,導致當前系統不可用的問題。

  • 開放主機服務(Open Host Service):通過定義一種協議,並公開給需要與之集成的下游系統,作為系統間通訊方式。如WCF、Web API、MQ。

  • 發布語言(Published Language):將當前限界上下文中的模型,轉換為整個領域下的全局概念的模型(DTO模型或應用級別的消息),便於集成方系統理解。一般配合 開發主機服務 一起使用。

  • 另謀他路(SeparateWay):如果2套系統沒有關聯性,完全可以做到解耦,那么就沒必要集成,畢竟集成的代價總是昂貴的。

  • 大泥球(Big Ball of Mud):當前系統包含了多個領域模型,模型之間界限很模糊、耦合度高。面對這類系統,如果要與之集成,則應從這個系統的邊界作為整體來考慮,不要試圖嘗試對內部進行划分。如果有計划考慮重構系統,對包含的多個模型,進行剝離,那么待獨立出新的系統后,再重新做集成,這個是可取的。

Bounded Context

架構

架構,是用於解決非功能性需求而提出的一系列解決方案。以下的架構,在實際應用中,不是單獨存在的,往往一個系統會采用幾種架構的組合。

好的架構,在於選型是否合適,而非架構本身的優劣。

分層

所有架構的始祖。支持N層架構系統,被廣泛應用於Web、企業級應用。傳統的分層架構中,往往基礎設施作為依賴關系的最底層。隨着依賴注入(DI)的出現,通過依賴倒置原則,在領域驅動設計上,可以將領域層,作為整個項目的依賴最底層。目的呢,是為了讓領域層不依賴基礎設施層,從而在建模期間,可以將重心放在建模本身。

六邊形架構(Port And Adapter)

服務端開放端口,對外提供服務;客戶端通過適配器,使得可以使用服務端的服務。之前講限界上下文映射時提到的集成模式中的 防腐層,在此處即為適配器的實現。

這是DDD的首選架構,即主架構。

面向服務架構(SOA)

基於WebService、WCF的面向服務的開發。這塊目前正逐漸被微服務架構所取代,就不細講了。

微服務架構(Microservice)

通過HTTP RESTful、MQ消息隊列協議,實現服務之間交互。此架構的優勢在於可以水平拆分無狀態服務,實現可伸縮性、高可用性。一個限界上下文,可以被拆分成多個微服務。

命令查詢職責分離(CQRS)

將數據修改和數據查詢進行分離,天然得支持讀寫分離。優勢在於可以分別從C端、Q端優化數據結構,提高系統的可用性。

C端的使用,每次的數據修改,會伴隨領域事件的發布,通過對領域事件的訂閱,完成Q端數據的更新。

如果Q端的數據是異步更新的,那么會存在數據延遲刷新問題,其實這個問題在所有讀寫分離的使用場景都會存在,解決辦法也多種多樣。如客戶端增加延遲刷新頁面、頁面刷新時顯示最后一次Q端數據更新的時間,以消除使用者疑惑。

事件驅動架構(EDA)

通過事件、事件處理器,來驅動系統的運行。

此架構在實際應用場景,考慮到內存消息的易失性和服務集群部署的特點,往往會引入消息中間件。

  • 管道和過濾器(Pip、Filter)

如果用管道來類比的話:事件作為管道的輸入;事件處理器作為管道,來處理事件;管道的輸出,以新的事件的形式發布,再由關注此類事件的事件處理器進行處理。

這些事件處理,可以串行處理,也可以並行處理(當單個處理比較耗時,且相互之間不需要等待處理結果,可以並行,以縮短整體響應時長)。

  • 長時處理過程(Saga)

類比工作流:接收到特定的事件,作為流程中節點的流轉的條件;直到整個流程結束,那么這個長時處理過程,即完成。

使用場景:多聚合根之間交互的最終一致性實現;跨限界上下文交互的最終一致性實現(屬於分布式最總一致性范疇)。

  • 事件源(Event Sourcing)

記錄每次導致領域對象發生變化的事件。隨着人們要求更多的變化跟蹤信息,在業務層也需要記錄相應量的元數據信息。特別像財務這些對於金額變化比較敏感的業務,當需要引入審計功能時,對於具體發生金額變化的原始信息需要記錄。那么引入事件源的概念,就再適合不過。

事件源數據,是只追加型方式存儲的。隨着時間推移,數據量會越來越大,對於頻繁訪問的業務對象,也會產生性能影響。通過引入事件快照,可以解決此類性能問題。

數據量的增大,可以采用支持分布式存儲的存儲引擎,如Mongodb,或者對基於關系型數據庫實現的分庫分表,以解決數據量大的問題。

數據網織和基於網絡的分布式計算(Data Fabric)

簡單來講,通過引入本地緩存、分布式緩存的概念,將經常訪問的業務對象,存儲其中。將業務對象持久化到緩存中,相對關系型數據庫表而言,要容易的多。此處的業務,特指聚合根對象,因此對象緩存,我們也可以稱之為聚合存儲(Aggregate Store)。

如分布式大數據的商品產品 Gemfire,支持數據復制、持續查詢、分布式處理。

將此概念結合CQRS使用,可以最大化提升C端的性能。

  • 數據復制

使用 “一個緩存對應一個聚合”的策略時,如果這個緩存存在於一個分布式緩存節點,那么會存在單點問題。引入多節點(冗余節點)時,需要考慮多節點之間的數據復制問題,以確保數據同步。

  • 事件驅動網織和領域事件

可以很好結合 事件驅動架構。通過發布領域事件,到支持按事件更新分布式緩存;分布式緩存過期,由數據網織服務發布“重新初始化緩存”事件,再由業務系統通過訂閱后,進行緩存更新。這種相對傳統的完整更新緩存而言,網絡數據傳輸量可以降低。

  • 持續查詢

有些數據網織支持一種名為持續查詢(Continuous Query)的事件通知。客戶端向數據網織注冊一個查詢,當對緩存進行修改時,客戶端可以收到事件通知,結合業務本身來判斷是否需要通知用戶界面組件。

  • 分布式處理

可以在所有緩存節點范圍內完成分布式處理,然后將處理結果聚合到一起發給客戶端。如 事件驅動架構 里提到的 長時處理過程,可以將幾個並行的事件處理轉為一個函數服務(Function Service),注冊到數據網織中,最終結果將以事件形式發布。

實戰演練

用我熟悉的電商領域中的庫存子域作為核心域,來列舉幾個業務場景:

  • 1)用戶下單后,通知庫存子域去凍結訂單庫存;超時1小時未付款的,則自動取消訂單,並解凍訂單庫存

  • 2)將相同訂單來源、收貨人姓名、收貨人手機號、收件人地址的訂單,合並成一張出貨單,以降低發貨成本

  • 3)出貨單,按照訂單付款時間,依次占用倉庫庫存

    • a)如果庫存夠,則訂單轉為出庫中;倉庫的揀貨區庫存充足時,出貨單轉為待下貨,對應的訂單商品解凍訂單庫存;倉庫揀貨區庫存不足時,出貨單轉為待調撥,訂單庫存保持凍結狀態

    • b)如果庫存不夠,則訂單狀態仍為待出庫,出貨單則轉為待采購,並通知采購子域,缺貨的商品及數量

  • 4)將發貨商品一樣的,或發貨商品所在同一個揀貨子區域的出貨單,合並成一張揀貨總單,以提高揀貨效率

  • 5)倉庫揀貨人員在揀貨區域,按照揀貨總單上的單號、存放商品的盒子條碼進行下貨,並告知揀貨總單下的各個出貨單需揀貨的數量,完成多個出貨單的揀貨過程。

問題:就以上的業務場景,列一下涉及哪幾個子域,子域之間的上下游關系?哪些是支撐子域,哪些是核心域,哪些是通用子域。

order-inventory-purchase

好了,這篇講了DDD的戰略內容大部分,之所以先將這些,是因為這些是開發人員和產品經理都應該知道的,而且能大致了解DDD里都有哪些東西。

下一篇,會從DDD戰術角度來說明。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM