DDD-領域驅動設計簡談
看到網上討論 DDD 的文章越來越多,咱也不能甘于人后啊,以下是我對 DDD 的個人理解,短小精悍,不喜忽噴。
解決什么問題
傳統模式,產品評審結束,開發人員就憑經驗拆分模塊,設計數據結構,然后寫業務邏輯實現功能。問題在于,不同人的經驗、理念不一樣,同樣的產品需求,最終的技術實現也會不一樣;就算是同一個人,可能不同時候接手同樣的需求,也會出來不同的設計。究其原因,很多細節之處都是拍腦袋或按個人喜好,或以無所謂的心態處理了,得出的自然是各式各樣的結果。往往這些結果是無法令人滿意的,這又觸發了重構的沖動,然而由于沒有一套標準的原則和方法論,所謂的重構只不過是周而復始,盲人探路。
DDD(領域驅動設計)的出現,猶如黑暗中的燈塔,點燃了希望,指引了方向!
其實在傳統模式中,我們已經有領域概念了,因為領域概念是天然的,自然地充斥在需求的各個角落。在設計數據庫的時候,開發人員肯定是以自己經驗,按領域模型構建表結構的。比如用戶表、訂單表、訂單子表等等,這里的用戶、訂單、訂單子項就是領域模型。但領域驅動到此為止,一旦數據庫設計完畢,以數據為驅動的開發模式就粉墨登場并貫穿整個項目周期,所有操作都開始圍繞數據庫作增刪改查。到后期數據量大起來,業務外拓,怎么迭代、怎么重構又是公說公有理婆說婆有理,一團漿糊。可能每個人說的都有道理,各自的方案都是可行的,但問題就出在都有道理上,誰也說服不了誰。于是我們迫切地需要一套方法,統一思路,統一方向,不同的人借助它都能設計出較為合理的架構,且相互之間可以認同,就算有調整也是細節而非大的層面。
DDD 就是這樣一套方法,如果高內聚、低耦合是理想的架構,那么 DDD 就是為了實現它形成的一套方法論。它作用于需求分析、產品分解、架構設計、業務編寫等項目環節,開發人員至少從產品分解環節就要介入。借助 DDD,你會發現混沌迷蒙的代碼世界從來沒有如此清晰,一條康莊大道在你眼前鋪開,一路延伸到那看不見的遠方。
概念
首先來看下領域模型的四個概念:
值對象:不可變,意即改變其狀態等于是得到了一個新的值對象。外部不是以引用去引用值對象(好怪),而是直接使用它本身。(題外話:C# 9 引入的record有一點值對象的意思; ef core 中新引入的 Owned Entity Types 即是值對象在 ORM 中的技術實現;但是 Owned Entity 有其特性 —— 無法同時屬于多個主實體 —— 可能在某些場合不便,ef core 8 又引入了 ComplexProperty)
實體:有狀態(即有生命周期),有標識符(比如 id)。
聚合:聚合是業務和邏輯緊密關聯的實體和值對象組合而成,聚合是數據修改和持久化的基本單元。
聚合根:也是實體,同時是聚合的管理者。在聚合內部,負責協調實體和值對象按照固定的業務規則協同完成共同的業務邏輯;在聚合之間,它是聚合對外的接口人,以聚合根id的方式接受外部請求和任務,實現上下文中的聚合之間的業務協同。
DDD 中的領域模型是充血模型。
舉例
訂單有待付款、已付款、已完成等狀態,是實體;
訂單還有訂單子項集合(每一項對應一個商品),每個訂單子項可以增減數量(也即改了屬性,但還是那個訂單子項),所以也是實體;
訂單有收貨地址,收貨地址由省、市、區、具體住址組成,各部分可獨立設置,當修改了其中一項,自然就是一個新地址了,所以收貨地址是值對象;
訂單子項和收貨地址依賴于訂單,不能獨立存在,外部自然也不能繞過訂單直接操作到它們,因此訂單、訂單子項、收貨地址可作為一個聚合,訂單是聚合根。
有同學會說,不對啊,收貨地址在我的模塊里是可以修改維護的呀,修改之后記錄還是原來的記錄(同個對象),只是內容不一樣呀(狀態變化),按你的說法,收貨地址應該是實體才對。——這就是不同的限界上下文導致同樣的業務概念表現為不同的領域模型,甚至在不同的聚合中也可以不同——假設訂單引用了收貨地址(用戶選擇了收貨地址列表中的第一條北京王府井),如果此時有個操作更改了該收貨地址(用戶修改了他的第一條收貨地址,從北京王府井改為杭州延安路),那么原來已確定的訂單地址會跟著變嗎?顯然不會。這就是值對象的概念。其實值對象和實體同我們熟悉的值類型、引用類型表現行為是差不多的。
對于值對象以及其它所有概念,我們要理解,而不是刻意套用。
我們再來想,購物車是否可以劃入訂單聚合里,畢竟感覺上購物車就是為訂單服務的。這里有個簡單的判斷準則:領域模型是否可以獨立訪問,是就是聚合。如果沒有訂單,購物車是可以存在的;反之,用戶可以直接下單,而不需要先把商品加入到購物車;所以購物車和訂單分屬兩個聚合。
當在編碼過程中發現單次請求涉及到一個服務中的多個聚合操作,這肯定是有問題的。要么是前端業務操作未按照領域規劃拆解,要么是產品將原本屬于同一聚合的業務過度拆分了。
DDD 是以高內聚、低耦合為指向的。可以說,這兩者是絕大多數架構水平的評判標準,自然也是 DDD 的理論基礎。
低耦合:模塊之間不依賴對方的具體實現。我們熟悉的面向接口編程、IOC等機制就是為了貫徹它而來的,曾經流行一時的幾十種面向對象設計模式大多也是為了達到低耦合的目的。
高內聚:模塊只負責自己應該負責的職責。高內聚與低耦合關注點不同,它是劃分模塊職責的原則。
一般來講,高內聚、低耦合是相輔相成的,高內聚決定了必須低耦合(A不能直接調用B,否則A可以行使B的職責),低耦合要求高內聚(既然A、B互不關聯,那必然職責得是分離的)。
許多人對它們區分不清,特別是高內聚(畢竟低耦合一直被強調),這里簡單說明:比如 A、B 能否直接相互依賴,這是低耦合的考量;A、B 是否能整合成C,或者A是否需要拆分為 C、D,這是高內聚的考量。領域分析時,高內聚會考慮多一點,構建代碼時,就要考慮這些職責不同的模型如何協同工作,也即如何耦合到一起。
DDD 提出了兩種模式:
領域服務:當領域中的某個操作過程或轉換過程不是實體或值對象的職責時,此時我們便成該將該操作放在一個單獨的接口中,即領域服務。 領域服務是無狀態的。一種情況是當領域層需要外部依賴(比如根據異步結果決定是否更改領域狀態,個人傾向于這部分邏輯置于應用層;如果需要防止領域知識泄漏,并將所有領域邏輯保留在領域模型邊界內,那么這部分邏輯可置于領域服務),或一個業務需要多個聚合參與時,參看領域服務與應用服務的區別
或更簡單地概括:
- 有的業務需同一聚合的A和B兩個實體共同完成,就可將這段業務邏輯用領域服務實現
- 有的業務需聚合C和聚合D中的兩個服務共同完成,使用應用服務來組合這倆服務
領域事件:一般指子域內部事件。當A模塊執行完自己的操作后,觸發事件,任何監聽該事件的模塊開始執行自己的操作。
這兩種模式又引出了CQRS的概念,如此又可以自然地去考量基礎層數據源的劃分和同步方案……
上文提到的限界上下文(BC):系統內部按照不同業務目的進行劃分的模塊。這里等同子域的概念,子域屬于業務范疇,而BC就觸及到服務的領域了。整個領域可以拆分為多個子域,子域又可再往下細分顆粒度更小的子域,最終層的每個子域,對應一個微服務(最小顆粒度的BC對應的服務)。
子域又分核心域、支撐域、通用域,這又是 DDD 創造的一些概念,目的仍是為了按一定特征劃分業務關系,千萬不要覺得這是什么高深術語。
另外提一嘴,微服務之間如果通過 SDK/API 方式互相調用的話,首先要判斷是否子域劃分得有問題,一個業務不應該強關聯多個微服務;對于必須跨服務執行的情況,除直接調用外,也可以考慮事件總線的方案,但是否值得這么做需要考量。一般事件總線用于弱關聯服務之間,比如訂單服務、消息推送服務,一旦有顧客下單,馬上給商家推送消息,它們之間雖然有順序關系,但上游服務并不關心下游服務是否執行到位;對于強關聯服務,要考慮事務的最終一致性。
相關資料
如何運用領域驅動設計 - 值對象 (文中說的盡量避免使用基元類型不敢茍同,屬性若是基元類型就能表示清楚且滿足功能需求的沒必要非得封裝為值類型,而且最后一層的屬性肯定只能是基元類型)
DDD之4聚合和聚合根
MASA Framework - DDD設計(1)
eShopOnContainers 知多少8:Ordering microservice
阿里技術專家詳解DDD系列 第三講 - Repository模式

浙公網安備 33010602011771號