為什麼我越來越喜歡用DDD — DDD架構篇(1)

DOONDO發表於2024-10-30

Hello DDD

  1. DDD 是一種軟體設計方法,DDD 是指導我們做軟體工程設計的一種手段。它提供了用切割工程模型的各類技巧,如;領域、界限上下文、實體、值物件、聚合、工廠、倉儲等。透過 DDD 的指導思想,我們可以在前期投入更多的時間,更加合理的規劃出可持續迭代的工程設計。
  2. 在DDD中有一套共識的工程兩階段設計手段,包括:戰略設計、戰術設計
    • 戰略設計,DDD架構主要應對複雜的業務系統需求,透過抽象、分治的過程,合理的拆分為獨立的多個微服務,從而分而治之。與之評價拆分的是否合理,則是在需求開發上線時候,是否每次都大量操作多個微服務開發和上線。這樣的戰略設計是一種失敗的微服務單體設計。所以少數幾個中等規模的單體應用,周圍環繞著一個服務生態系統,這更有意義。
    • 戰術設計:在這個範疇下,主要以討論如何基於物件導向思維,運用領域模型來表達業務概念。常在不做領域模型設計的架構,也就是通常對映到 MVC 三層架構下,Service + 資料模型的開發模式,會讓 Service 扁平的、大量的,平鋪出非常複雜的業務邏輯程式碼。再加上行為物件與功能邏輯的分離,貧血模型的開發方式,讓行為物件的不斷交叉使用,也是讓系統不斷增加複雜度,併到難以維護的根因。所以這一階段要設計每一個可以表達領域概念的模型,並運用實體、聚合、領域服務來承載。

DDD的概念

什麼是充血模型?

領域內都包括什麼?

實體、聚合、值物件,有什麼區別?

這些概念,也是戰術設計過程中非常重要的知識項,搞清楚它們才能做 DDD 設計。

充血模型

充血模型,指將物件的屬性資訊與行為邏輯聚合到一個類中,常用的手段如在物件內提供屬於當前物件的資訊校驗拼裝快取Key不含服務介面呼叫的邏輯處理等。

  • 這樣的方式可以在使用一個物件時,就順便拿到這個物件的提供的一系列方法資訊,所有使用物件的邏輯方法,都不需要自己再次處理同類邏輯。
  • 但不要只是把充血模型,僅限於一個類的設計和一個類內的方法設計。充血還可以是整個包結構,一個包下包括了用於實現此包 Service 服務所需的各類零部件(模型、倉儲、工廠),也可以被看做充血模型。
  • 同時我們還會再一個同類的類下,提供對應的內部類,如使用者實名,包括了,通訊類、實名卡、銀行卡、四要素等。它們都被寫進到一個使用者類下的內部子類,這樣在程式碼編寫中也會清晰的看到子類的所屬資訊,更容易理解程式碼邏輯,也便於維護迭代。

領域模型

領域模型,指特定業務領域內,業務規則、策略以及業務流程的抽象和封裝。在設計手段上,透過風暴模型拆分領域模組,形成界限上下文。最大的區別在於把原有的眾多 Service + 資料模型的方式,拆分為獨立的有邊界的領域模組。每個領域內建立自身所屬的;領域物件(實體、聚合、值物件)、倉儲服務(DAO 操作)、工廠、埠介面卡Port(呼叫外部介面的手段)等。

那麼,現在這裡有幾個概念;領域服務、領域物件、倉儲定義、事件訊息、埠介面卡。我們先來看他們是怎麼從貧血模型演變過來的,在細分講解每個概念。

  • 在原本的 Service + 貧血的資料模型開發指導下,Service 串聯呼叫每一個功能模組。這些基礎設施(物件、方法、介面)是被相互掉呼叫的。這也是因為貧血模型並沒有物件導向的設計,所有的需求開發只有詳細設計。
  • 換到充血模型下,現在我們以一個領域功能為聚合,拆分一個領域內所需的 Service 為領域服務,VO、Req、Res 重新設計為領域物件,DAO、Redis 等持久化操作為倉儲等。舉例;一套賬戶服務中的,授信認證、開戶、提額降額等,每一個都是一個獨立的領域,在每個獨立的領域內,建立自身領域所需的各項資訊。
  • 領域模型還有一個特點,它自身只關注業務功能實現,不與外部任何介面和服務直連。如;不會直接呼叫 DAO 操作庫,也不會呼叫快取操作 Redis,更不會直接引入 RPC 連線其他微服務。而是透過倉庫和埠介面卡,定義呼叫外部資料的含有出入參物件的介面標準,讓基礎設施層做具體的呼叫實現。透過這樣的方式讓領域只關心業務實現,同時做好防腐。

實體、聚合、值物件

原本在貧血模型下的開發,常常是不會特別在意一個方法的出入參物件的,也經常是很多個服務共用一個VO物件作為入參,只要這個物件能把我需要的屬性資訊帶進來就可以了。

但在 DDD 的領域模型設計下,領域物件的設計是非常物件導向的。而且在整個風暴事件的四色建模過程也是在以領域物件為驅動進行的。

實體、聚合、值物件,三者位於每個領域下的領域物件內,服務於領域內的領域服務。三個物件定義具體如下;

實體

是依託於持久化層資料以領域服務功能目標為指導設計的領域物件。持久化PO物件是原子類物件,不具有業務語義,而實體物件是具有業務語義且有唯一標識的物件,跟隨於領域服務方法的全生命週期物件。如;使用者PO持久化物件,會涵蓋,使用者的開戶實體、授信實體、額度實體物件。也包括如商品下單時候的購物車實體物件。這個物件也通常是領域服務方法的入參物件。

  • 概念:實體 = 唯一標識 + 狀態屬性 + 行為動作(功能),是DDD中的一個基本構建塊,它代表了具有唯一標識的領域物件。實體不僅僅包含資料(狀態屬性),還包含了相關的行為(功能),並且它的標識在整個生命週期中保持不變。
  • 特徵:
    • 唯一標識:實體具有一個可以區分其他實體的識別符號。這個識別符號可以是一個ID、一個複合鍵或者是一個自然鍵,關鍵是它能夠唯一地標識實體例項。
    • 領域標識:實體的標識通常來源於業務領域,例如使用者ID、訂單ID等。這些識別符號在業務上有特定的含義,並且在系統中是唯一的。
    • 委派標識:在某些情況下,實體的標識可能是由ORM(物件關係對映)框架自動生成的,如資料庫中的自增主鍵。這種識別符號雖然可以唯一標識實體,但它並不直接來源於業務領域。
  • 用途:
    • 表達業務概念:實體用於在軟體中表達具體的業務概念,如使用者、訂單、交易等。透過實體的屬性和行為,可以描述這些業務物件的特徵和能力。
    • 封裝業務邏輯:實體不僅僅承載資料,還封裝了業務規則和邏輯。這些邏輯包括驗證資料的有效性、執行業務規則、計算屬性值等。這樣做的目的是保證業務邏輯的集中和一致性。
    • 保持資料一致性:實體負責維護自身的狀態和資料一致性。它確保自己的屬性和關聯關係在任何時候都是正確和完整的,從而避免資料的不一致性。
  • 實現手段:
    • 定義實體類:在程式碼中定義一個類,該類包含實體的屬性、建構函式、方法等。
    • 實現唯一標識:為實體類提供一個唯一標識的屬性,如ID,並確保在實體的生命週期中這個標識保持不變。
    • 封裝行為:在實體類中實現業務邏輯的方法,這些方法可以操作實體的狀態,並執行相關的業務規則。
    • 使用ORM框架:利用ORM框架將實體對映到資料庫表中,這樣可以簡化資料持久化的操作。
    • 實現領域服務:對於跨實體或跨聚合的操作,可以實現領域服務來處理這些操作,而不是在實體中直接實現。
    • 使用領域事件:當實體的狀態發生變化時,可以釋出領域事件,這樣可以通知其他部分的系統進行相應的處理。

值物件

這個物件在領域服務方法的生命週期過程內是不可變物件,也沒有唯一標識。它通常是配合實體物件使用。如為實體物件提供物件屬性值的描述,比如;一個公司僱員的級別值物件,一個下單的商品收貨的四級地址資訊物件。所以在開發值物件的時候,通常不會提供 setter 方法,而是提供建構函式或者 Builder 方法來例項化物件。這個物件通常不會獨立作為方法的入參物件,但做可以獨立作為出參物件使用。

  • 概念:值物件是由一組屬性組成的,它們共同描述了一個領域概念。與實體(Entity)不同,值物件不需要有一個唯一的識別符號來區分它們。值物件通常是不可變的,這意味著一旦建立,它們的狀態就不應該改變。
  • 特徵:
    • 不可變性(Immutability):值物件一旦被建立,它的狀態就不應該發生變化。這有助於保證領域模型的一致性和執行緒安全性。
    • 等價性(Equality):值物件的等價性不是基於身份或引用,而是基於物件的屬性值。如果兩個值物件的所有屬性值都相等,那麼這兩個物件就被認為是等價的。
    • 替換性(Replaceability):由於值物件是不可變的,任何需要改變值物件的操作都會導致建立一個新的值物件例項,而不是修改現有的例項。
    • 側重於描述事物的狀態:值物件通常用來描述事物的狀態,而不是事物的唯一身份。
    • 可複用性(Reusability):值物件可以在不同的領域實體或其他值物件中重複使用。
  • 用途:
    • 金額和貨幣(如價格、工資、費用等)
    • 度量和資料(如重量、長度、體積等)
    • 範圍或區間(如日期範圍、溫度區間等)
    • 複雜的數學模型(如座標、向量等)
    • 任何其他需要封裝的屬性集合
  • 實現手段:
    • 定義不可變類:確保類的所有屬性都是私有的,並且只能透過建構函式來設定。
    • 重寫equals和hashCode方法:這樣可以確保值物件的等價性是基於它們的屬性值,而不是物件的引用。
    • 提供只讀訪問器:只提供獲取屬性值的方法,不提供修改屬性值的方法。
    • 使用工廠方法或建構函式建立例項:這有助於確保值物件的有效性和一致性。
    • 考慮序列化支援:如果值物件需要在網路上傳輸或儲存到資料庫中,需要提供序列化和反序列化的支援。

聚合

當你對資料庫的操作需要使用到多個實體時,可以建立聚合物件。一個聚合物件,代表著一個資料庫事務,具有事務一致性。聚合中的實體可以由聚合提供建立操作,實體也被稱為聚合根物件。一個訂單的聚合,會涵蓋;下單使用者實體物件、訂單實體、訂單明細實體和訂單收貨四級地址值物件。而那個作為入參的購物車實體物件,已經被轉換為實體物件了。—— 聚合內事務一致性,聚合外最終一致性。

  • 概念:聚合是領域模型中的一個關鍵概念,它是一組具有內聚性的相關物件的集合,這些物件一起工作以執行某些業務規則或操作。聚合定義了一組物件的邊界,這些物件可以被視為一個單一的單元進行處理。
  • 特徵:
    • 一致性邊界:聚合確保其內部物件的狀態變化是一致的。當對聚合內的物件進行操作時,這些操作必須保持聚合內所有物件的一致性。
    • 根實體:每個聚合都有一個根實體(Aggregate Root),它是聚合的入口點。根實體擁有一個全域性唯一的識別符號,其他物件透過根實體與聚合互動。
    • 事務邊界:聚合也定義了事務的邊界。在聚合內部,所有的變更操作應該是原子的,即它們要麼全部成功,要麼全部失敗,以此來保證資料的一致性。
  • 用途:
    1. 1.封裝業務邏輯:聚合透過將相關的物件和操作封裝在一起,提供了一個清晰的業務邏輯模型,有助於業務規則的實施和維護。
    2. 2.保證一致性:聚合確保內部狀態的一致性,透過定義清晰的邊界和規則,聚合可以在內部強制執行業務規則,從而保證資料的一致性。
    3. 3.簡化複雜性:聚合透過組織相關的物件,簡化了領域模型的複雜性。這有助於開發者更好地理解和擴充套件系統。
  • 實現手段:
    • 定義聚合根:選擇合適的聚合根是實現聚合的第一步。聚合根應該是能夠代表整個聚合的實體,並且擁有唯一標識。
    • 限制訪問路徑:只能透過聚合根來修改聚合內的物件,不允許直接修改聚合內部物件的狀態,以此來維護邊界和一致性。
    • 設計事務策略:在聚合內部實現事務一致性,確保操作要麼全部完成,要麼全部回滾。對於聚合之間的互動,可以採用領域事件或其他機制來實現最終一致性。
    • 封裝業務規則:在聚合內部實現業務規則和邏輯,確保所有的業務操作都遵循這些規則。
    • 持久化:聚合根通常與資料持久化層互動,以儲存聚合的狀態。這通常涉及到物件-關係對映(ORM)或其他資料對映技術。

倉儲和介面卡

在 DDD 的設計方法中,領域層做到了只關心領域服務實現。最能體現這樣設計的就是倉庫和介面卡的設計。通常在 Service + 資料模型的設計中,會在 Service 中引入 Redis、RPC、配置中心等各類其他外部服務。但在 DDD 中,透過倉儲和介面卡以及基礎設施層的定義,解耦了這部分內容。

  • 特徵:
    • 封裝持久化操作:Repository負責封裝所有與資料來源互動的操作,如建立、讀取、更新和刪除(CRUD)操作。這樣,領域層的程式碼就可以避免直接處理資料庫或其他儲存機制的複雜性。
    • 領域物件的集合管理:Repository通常被視為領域物件的集合,提供了查詢和過濾這些物件的方法,使得領域物件的獲取和管理更加方便。
    • 抽象介面:Repository定義了一個與持久化機制無關的介面,這使得領域層的程式碼可以在不同的持久化機制之間切換,而不需要修改業務邏輯。
  • 用途:
    • 資料訪問抽象:Repository為領域層提供了一個清晰的資料訪問介面,使得領域物件可以專注於業務邏輯的實現,而不是資料訪問的細節。
    • 領域物件的查詢和管理:Repository使得對領域物件的查詢和管理變得更加方便和靈活,支援複雜的查詢邏輯。
    • 領域邏輯與資料儲存分離:透過Repository模式,領域邏輯與資料儲存邏輯分離,提高了領域模型的純粹性和可測試性。
    • 最佳化資料訪問:Repository實現可以包含資料訪問的最佳化策略,如快取、批處理操作等,以提高應用程式的效能。
  • 實現手段:
    • 定義Repository介面:在領域層定義一個或多個Repository介面,這些介面宣告瞭所需的資料訪問方法。
    • 實現Repository介面:在基礎設施層或資料訪問層實現這些介面,具體實現可能是使用ORM(物件關係對映)框架,如MyBatis、Hibernate等,或者直接使用資料庫訪問API,如JDBC等。
    • 依賴注入:在應用程式中使用依賴注入(DI)來將具體的Repository實現注入到需要它們的領域服務或應用服務中。這樣做可以進一步解耦領域層和資料訪問層,同時也便於單元測試。
    • 使用規範模式(Specification Pattern):有時候,為了構建複雜的查詢,可以結合使用規範模式,這是一種允許將業務規則封裝為單獨的業務邏輯單元的模式,這些單元可以被Repository用來構建查詢。

Repository模式是DDD(領域驅動設計)中的一個核心概念,它有助於保持領域模型的聚焦和清晰,同時提供了靈活、可測試和可維護的資料訪問策略。

倉儲解耦的手段使用了依賴倒置的設計,所有領域需要的外部服務,不在直接引入外部的服務,而是透過定義介面的方式,讓基礎設施層實現領域層介面(倉儲/介面卡)的方式來處理。

那麼也就是基礎設定層負責原則對接資料庫快取配置中心RPC介面HTTP介面MQ推送等各項資源,並承接領域服務的介面呼叫各項服務為領域層提供資料能力。

同時這也會體現出,領域層的實現是具有業務語義的,而到了基礎設施層則沒有業務語義,都是原子的方法。透過原子方法的組合為領域業務語義提供支撐。

領域編排

在 DDD 中,每一個領域都是界限上下文拆分的獨立結果,而實現業務流程的功能則需要串聯各個領域模組提供一整條鏈路的完整服務。所以也常說領域內事務一致性,領域外最終一致性。

同時這些領域模組因為是獨立的,所以也可以被複用。在不同的場景功能訴求下,可以選擇不同的領域模組進行組裝,這個過程就像搭積木一樣。

但這裡有一個取捨,如果專案相對來說並不大,也沒有太多的編排處理。那麼可以直接讓觸發器層對接領域層,減少編排層後,編碼會更加便捷。

觸發器

在所有的模型都定義完成後,領域業務被串聯了。那麼接下來則是使用,而使用的方式可以包括;介面(http/rpc)、訊息監聽、定時任務等方式,這些方式統一被定義為觸發動作。

由觸發發起對編排功能的呼叫處理。如;定時任務做信貸的計息、開戶成功訊息通知返利優惠券、提供介面讓外部呼叫授信邏輯等。這些都是觸發動作。

相關文章