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