淺析 DDD

xuding發表於2021-09-07


本文轉載自【何以解耦】:codedecoupled.com/intro-ddd.html

DDD 是什麼(首先它不是什麼)

DDD 是 Domain Driven Design 的縮寫,在中文中常被翻譯為領域驅動設計。在我們瞭解 DDD 是什麼之前,首先討論下它不是什麼。

  • DDD 不是一個軟體框架。但是基於 DDD 思想的框架是存在的,比如 Axon,它是以 DDD 為指導思想,使用 Java 實現的一個微服務軟體框架。
  • DDD 不是一種軟體設計模式。它不是像工廠,單例這樣子的設計模式。但是 DDD 思想中提出了諸如資源庫(Repository)之類的設計模式。
  • DDD 不是一種系統架構模式。它不是像 MVC 之類的架構模式。但是 DDD 思想中提出了諸如事件溯源(Event Souring),讀寫隔離(Command Query Responsibility Segregation) 之類的架構模式。

那麼 DDD 到底是什麼

軟體是服務於人類,為提高人類生產效率而產生的一種工具, 每一個軟體都服務於某一個特定的領域。比如一個 CRM,它是以管理客戶資料為核心,幫助商戶與客戶保持聯絡的工具。

而軟體的實質是計算機中執行的程式碼,如何將抽象的程式碼更準確地對映到人類所關心的領域中,這是軟體開發者一直在探尋的話題,不管是函數語言程式設計(FP)還是物件導向程式設計(OOP)也好,都是為了幫助開發者開發出更貼近於領域中的軟體模型。

在傳統的軟體開發方法中,我們常常會遇到一系列影響軟體質量的技術以及非技術問題:

  • 開發者熱衷於技術,但缺乏設計和業務思考。開發人員在不完全瞭解業務需求的情況下,閉門造車,即使功能上線也無人問津。
  • 程式碼輸入而非業務輸入。技術人員對技術實現情有獨鍾,出現殺雞焉用牛刀的情況。
  • 過於重視資料庫。以資料庫設計為中心,而非業務來進行開發,結果往往是,軟體無法適應一直在變動的業務邏輯。

DDD 是一種設計思想,一種以領域(業務)為出發點,以解決軟體建模複雜度為目的設計思想,我們也可以將其理解為一種建模的方法論。

DDD 的設計思想分為戰略和戰術兩部分。

我們可以將戰略設計理解為巨集觀層面的設計,它的目的包括,分析業務的複雜程度,拆分業務的領域範疇,指導業務整合方式等等 。我們可以將戰術設計理解為微觀層面(程式碼層面)的設計,它為我們在實現業務邏輯提供一系列工具。

戰略設計

通用語言(Ubiquitous Language)

語言本是人類溝通的基本工具,然而開發人員習慣了使用技術術語,領域專家(領域專家在此泛指精通業務的專家,比如使用者,客戶等等)對技術術語毫不關心,於是造成了不可避免的溝通問題,一旦溝通出現問題,開發出來的軟體便很難解決領域專家的真正痛點。

通用語言是 DDD 思想的基石,它是開發人員和領域專家共同建立一套溝通語言,一套在團隊中流行的,通用的溝通語言,團隊的組員之間可使用通用語言進行無障礙交流。

這要求開發人員摒棄技術味濃重的技術術語,與領域專家合作,像領域專家一樣關注業務問題,共同挖掘並打磨業務中的術語,建立一套通用語言。

通用語言往往可以直接應用於程式碼中,它可以直接被寫成一個類或者一個類的方法。

比如在開發一個購物車時,與其使用技術術語:

  • Cart::create(): 建立一個購物車。
  • Cart::updateStatus():更新購物車狀態。
  • Cart::remove():移除購物車。

我們不妨使用更貼近業務的通用語言:

  • Cart::init(): 建立一個購物車。
  • Cart::addItemToCart():新增商品。
  • Cart::removeItemFromCart():移除商品。
  • Cart::empty():清空購物車。

在使用後者時,開發人員不用解釋每一個類方法的意義,領域專家可以直接看懂每一個類方法的目的。開發人員甚至可以和領域專家坐在一起使用程式碼來打磨業務流程。

限界上下文(Bounded Context)

在實現了通用語言自由以後,我們需要使用限界上下文來規定每一套通用語言的使用邊界。限界上下文是語義和語境的邊界,在其內的每一個元素都有自己特定的含義,也就是說每一個概念在一個限界上下文中都是獨一無二,不可以出現一詞多義的情況。

我們可以用一個簡單的例子來解釋限界上下文。比如在一個購物車的限界上下文中,我們可以用 User 一詞來代表購買商品的客戶。而在一個註冊系統中,我們可以用 User 一詞指的是帶有使用者名稱和密碼的賬號。雖然詞彙一樣,但是在不同的限界上下文中,它們的含義不同。

我們使用限界上下文和通用語言,對業務進行語言層面的拆分。限界上下文為領域中的每一個元素賦予清晰的概念,開發人員也就不會將情不自禁的在腦海中閃現多個支撐一個元素的概念,避免寫出“大泥球”(big ball of mud)程式碼。

子域(Subdomain)

如果說限界上下文是對業務進行語言層面拆分的話,那麼子域便是對業務進行商業價值的拆分。每一個商業都有自己的關注點,即便是看起來一樣的電商平臺,淘寶是開放平臺模式,京東是價值鏈整合模式,一個明顯的區別是,淘寶使用第三方物流而京東自建物流體系。

那麼作為一個開發人員,為何要關心看起來似乎與自己無關的商業模式呢?恰恰相反,只有當我們瞭解一個商業的結構時,才能開發出一個主次分明的系統來支撐一個商業的飛速發展。

子域便是這樣一個幫助我們劃分主次的工具。

有三種型別的子域:

  • 核心域(Core Domain):這是系統中需要最大投資的領域,它代表著整個商業的核心競爭力。我們需要花大量資源以及資源來打磨核心域,這關乎一個企業的存亡。比如京東的自建物流系統。
  • 支撐域(Supporting Domain):此領域並非一個企業的核心業務,但是核心域卻離不開它,它可以採用外包定製方案實現。比如認證上下文,許可權上下文。
  • 通用域(Generic Domain):如果已有成熟的解決方案,通用域可以採購現成方案來,如果沒有,也可以採用外包,在通用域上的投資應該是最小的。比如對於淘寶而言,物流便是其通用域。

限界上下文和子域的關係眾說紛紜,有專家提倡1:1,也有專家提倡1:N。個人比較提倡 1:1, 由於受實現領域驅動設計一書作者影響比較深。

上下文對映(Context Mapping)

在一個龐大的系統中,限界上下文之間必定存在一定的依賴關係。如何將一個上下文中的概念對映到另一個上下文中?我們使用上下文對映。

以下是幾種上下文對映的關係型別:

  • 合作關係(Partnership)
  • 共享核心(Shared Kernel)
  • 客戶方-供應方開發(Customer-Supplier Development)
  • 遵奉者(Conformist)
  • 防腐層(Anticorruption Layer)
  • 開放主機服務(Open Host Service)
  • 釋出語言(Published Language)
  • 另謀他路(SeparateWay)
  • 大泥球(Big Ball of Mud)

以上都是比較抽象的概念,有時兩個限界上下文之間也可以存在多重關係。

以上便是 DDD 戰略設計的幾個核心概念。

戰術設計

如何為限界上下文中的概念建立模型,我們將使用的是 DDD 所提供的戰術設計。

實體(Entity)

首先我們講到的是,實體。

實體是領域中獨立事物的模型,每個實體都擁有一個唯一的識別符號,比如 ID, UUID,Username 等等。大多數情況下,實體是可變的,它的狀態會隨著時間的遷移改變,不過,一個實體不一定必須可變。

實體的最大的特徵是它的個體性,唯一性。比如在一個簡單的購物車上下文中,訂單(Order) 便是一個實體,ID 是它的識別符號,它的狀態可以在提交(placed),確認(confirmed) 以及已退 (refunded) 之間變化。

值物件(Value Object)

值物件是領域中用來描述,量化或者測量實體的模型。和實體不同,值物件沒有唯一的識別符號,兩個對等的值物件是可以替換的。值物件具有不變性(Immutability),一旦建立以後,一個值物件的屬性就定型了,不可更改。

理解值物件的最直接的方法是,想象我們現實生活中的鈔票,在日常生活中,甲的十塊錢人民幣和乙的十塊錢人民幣是可以對等交換的。

在上文的購物車上下文中,金額(Money)便是一個值物件,金額由貨幣(currency)和數目(amount):

class Money {
  public $currency;
  public $amount;

  function __construct($currency, $amount) {
    $this->currency = $currency;
    $this->amount = $amount;
  }
}

使用值物件進行建模,能幫助我們用程式碼更精確地建立領域模型。

聚合(Aggregate)

聚合是什麼?聚合是上下文中對業務領域更精細的劃分,每一個聚合保證自己的業務一致性。

那麼什麼是業務不變性?業務不變性表示一個業務規則,該規則在業務領域中不可違背,必須保證其一致性。比如,在進行訂單退款時,退款金額不可以超過已付金額。

聚合的組成部分是實體和值物件,有時候也只有實體。為了保護聚合的業務一致性,每個聚合只可以通過某一個實體對其進行操作,該實體被稱為聚合根。

領域事件(Domain Event)

領域事件是通過通用語言分析出來的事件,與常見的事務事件不同的是它與業務息息相關,所以它的命名往往夾帶業務名詞,而不應該與資料庫掛鉤。比如購物車增添商品,對應的領域事件應該是 ProductAddedToCart, 而不是 CartUpdated

總結

DDD 還提供了諸如應用服務(Application Service),領域服務(Domain Service) 等戰術設計,DDD 還提出了文章開頭就提過的事件溯源,六邊形等架構模式,在此我們將不一一介紹。

DDD 的核心是從業務的角度為軟體建立模型,其目的是打造更貼近業務的程式碼,能更直觀的從程式碼理清業務流程。 然而實現 DDD 並非一日之舉,它需要不斷的實踐,不斷的打磨。

本文轉載自【何以解耦】: codedecoupled.com/intro-ddd.html ,如果你也對 TDD,DDD 以及簡潔程式碼感興趣,歡迎關注公眾號【何以解耦】,一起探索軟體開發之道。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Know how, know why meanwhile.