DDD的戰術模式

banq發表於2018-11-21

DDD(領域驅動設計)是一種軟體設計方法的主張,這種方法非常全面,因為它提供了程式碼級別戰術、專案組織級別甚至整個組織的戰略級別的設計工具。Eric Evans 2003年的領域驅動設計:解決軟體核心的複雜性為DDD奠定了基礎。
該本書認為程式碼應該反映業務模式,技術問題應在專用開發下不會妨礙到業務領域。但要實現這一目標並非易事。實際上,經常會發生業務程式碼受到與技術方面相關的橫向侵入的影響(永續性 ......),意識到這種困難,Eric Evans在他的書中定義了許多模式,這些模式可以幫助我們在程式碼中表達業務問題:戰術模式。
為了熟悉戰術模式,我們在XKE期間在Xebia內組織了一個研討會,以探索和吸收Eric Evans提出的不同模式。研討會格式的靈感來自於所使用的[url=https://twitter.com/tpierrain/status/926216936316424192]2017年11月的聚會DDD巴黎[/url]。因此,在介紹由愛德華思哈克萊門特Heliou,團隊是分離成一個個小團隊,每一個研究的特定領域。然後,我們就這些不同的模式分享了我們的理解和意見。

實體
實體代表業務物件,其標識隨時間保持不變,不管他們的屬性如何。例如,如果必須為人類建模,他可以看到他的屬性變化(年齡,頭髮顏色......)。但他的標識仍然是一樣的。
因此,為了實現一個模型實體的程式碼,需要使用經典物件,其中所述的屬性將是恆定的並且將提供一個識別符號。因此,要知道兩個物件是否代表同一個實體,比較它們的識別符號就足夠了。在上一個人的例子中,我們可以選擇身份證號碼為標識,因為它具有恆定和獨特的優勢。
建模一個物件作為該實體需要翻譯尋找業務行業中的最佳概念。但它也有助於理解這種建模的含義。實際上,實體具有精確的生命週期,必須經常持久化,維護更復雜並且不是執行緒安全的(有必要避免程式的兩個元件同時修改同一個物件,因為導致不一致的風險)。
最後,請注意,在實現實體時,找到“自然”識別符號並不總是很容易。例如,如果像上面提到的,身份證號碼是一個很好的候選,我們想到姓名,但姓名很少是獨一無二的。最後,如果我們的應用程式必須生成識別符號,我們必須小心確保這些識別符號的唯一性,因為它並不總是不重要的:如果我們使用自增量識別符號,我們必須檢查分散式環境中增量的一致性; 如果我們使用時間戳,我們必須管理幾乎同時進行的查詢的情況......
我們理解為什麼實體的管理很複雜,以及為什麼我們在絕對必要時才使用實體,更喜歡值物件來建模業務物件。
例子
為了使這種模式更具體,讓我們以汽車領域為例。假設我們必須編寫管理汽車租賃公司的應用程式。汽車是一個不斷髮展的物件:它可以租用,零件可以更換等。因此很明顯,該物件將在應用程式中建模為實體。
與實體通常的情況一樣,您需要為每輛車找到一個識別符號。人們可以想到車牌,但這些車牌的規則因國家而異。幸運的是,車輛識別號碼作為自然識別符號,否則,有必要找到一個技術識別符號,其中包含驗證其所帶來的唯一性的所有問題。

值物件
值物件是由它們的屬性定義的業務物件:他們沒有標識。例如,如果我們更改日期的屬性(月,年......),我們會更改日期這個物件:但它不再是原來相同的日期物件了。因此,為了模擬這樣的物件,人們將使用不可變物件:當且僅當它們的所有屬性相等時,兩個物件才是相等的。因此,這些物件沒有隨著時間的推移而變化自己的內部狀態。
這些物件是不可變的這一事實使它們使用起來非常方便:它們是執行緒安全的,可以輕鬆轉移,等等。值物件不是簡單 DTO ; 我們可以(並且必須)將業務邏輯放在這些物件中。這甚至是吸收工作複雜性的好方法。
在專案中,區分作為實體的物件和作為值物件的物件是簡化程式碼和增加對業務領域的理解的最佳方法之一。
備註:

  • 值物件是一個很好的起點,可以定位哪些是值物件並將業務規則封裝在其中不僅可以更好地表達域,還可以“ 吸收 ”程式碼的複雜性。
  • 實體 / 值物件的區別是相對於上下文的:實際上,相同的概念可以由值物件或實體根據其中的域來表示。典型的例子是鈔票:對於商家而言,兩張相同價值的鈔票是可以互換的(值物件),而對於列印和銷燬這些鈔票的法蘭西銀行而言,特定的鈔票有一個識別符號和生命週期(實體)。
  • 最近,我們開始討論值物件而不是值型別,因為後一個術語是矛盾的(值型別是一個被認為具有一個內部狀態的物件,值物件是認為值不變的)。


例子:
讓我們留在上一個例子的汽車領域,並假設我們需要知道汽車的位置。汽車的位置是價值物件的一個很好的例子:一個位置沒有識別,完全由其值定義(用GPS座標表示)。也就是說,這個物件不一定是一個簡單的DTO,它可以包含許多具有商業意義的方法:烏鴉在兩個位置之間飛行的距離,兩個位置之間的最短路徑,這個職位所在的國家......

聚合Aggregate
在複雜的系統中(例如:分散式,多執行緒......),我們經常需要確保一組物件的一致性。但是,這種一致性很快就會成為維護程式碼的噩夢。
Aggregate模式提供瞭解決此問題的方法。聚合是一組業務物件(的值物件或實體連線在一起)。在這些物件,一個(通常是實體)將有一個特殊的角色:聚合根。其餘程式碼對Aggregate所做的任何更改都必須透過聚合根方法。因此,禁止透過直接修改非根物件來修改聚合。
因此,單獨的聚合根將負責確保聚合的一致性。一個聚合允許建立圍繞一個共同的組物件的周邊。因此,聚合 是實現業務不變和管理規則的好方法。為了確保一致性聚集在多執行緒環境中,其中的所有方法呼叫聚合內部物件應該是同步的。
一個很好的比喻來理解的原則,在動物園中幾個人來分配不同動物到籠子,如果時機不好,獅子和羚羊可能是在同一個籠子。最好問一下聚合根物件(這裡是動物園飼養員)來照顧放置籠養動物的一致性。
此外,聚合根是主要的實體,一個聚合是受到共同的約束:他們必須遵循一個生命週期。另一方面,如果程式必須處理多個聚合,則不推薦在多個節點/執行緒 之間共享此處理過程,每個聚合僅由一個節點修改。這種共享在真實世界中的很好的類比是:一個汽車工廠具有多個流水線,未來的車輛根據其識別符號分佈在各種組裝線,每一車輛只能透過定址一次對應到只有一條裝配線。
注意:因為一個聚合必須保證模型的一致性,它有時很有誘惑力,但確保聚合體儘可能小也很重要。實際上,聚合越大,維護越困難,並且呼叫只能同步增加的區域越多。

例子
在汽車相關業務領域的示例中,如果您想要詳細建模汽車,則必須考慮汽車包含相互通訊的部件這一事實。因此,汽車包含發動機,車輪,制動器。所有這些部件相互通訊:發動機使車輪加速,而制動器阻止它們,方向盤轉動它們。為了確保資料的一致性(避免汽車右轉彎,而車輪朝向左側),可以被建模為一個汽車聚合,外界只有透過汽車聚合根本身才可以訪問內部。

工廠
有時建立聚合(甚至特別大的值物件)可能非常複雜,特別是如果物件本身已經很複雜。
在這些情況下,Factory模式透過將建立物件的職責轉移到專用物件來提供解決方案。這是域模型的一部分,即使它在該領域確實沒有業務責任。因此,這種模式非常接近物件導向程式設計的經典GoF設計模式
但是,不應該為每個物件建立系統地使用此模式。否則,它將不必要地產生額外的複雜性。
請注意,使用模式Factory越來越謹慎,最常被模式Builder模式替換。後者在很大程度上是等同的,但具有更靈活的優點,並且介面通常更清晰。

例子
假設我們想編寫一個管理汽車租賃公司車隊的應用程式,那麼我們就將Car物件建模。但是,如果新車建立與指定發動機,剎車片和改造車體,不僅是複雜的(如前面的章節),還需要業務程式碼操縱Aggregates內部的物件。為避免這種情況,您可以賦予特定物件以“建立該物件”的責任,以將物件的建立與其餘業務程式碼隔離開來。

服務
在某些情況下,實體,值物件和聚合不足以包含業務域的所有邏輯。
在這種情況下,我們可以使用服務,這些服務是執行業務流程的類,這些業務流程無法由任何其他業務物件令人滿意地執行。這通常是將業務物件轉換為其他物件的情況。
另一個例子是兩個銀行賬戶之間的貨幣交易:哪個物件將處理程式碼級別的交易編排?在兩個“ 銀行賬戶 ”的物件之一中實施交易會很尷尬,因為在業務層面,這沒有多大意義。因此,在Services類中實現事務是一種更好的解決方案。
但是,請務必僅在任何業務物件(實體,值物件或聚合)無法實現的情況下考慮服務。否則可能導致貧血域模型,也就是說,實體變成簡單的DTO物件,所有的業務邏輯放入肥胖的服務類,這種情況可維護很差。
例子
仍然是汽車租賃公司的管理應用程式,假設我們想要建立一種計算汽車租賃價格的方法。對於這一點,就編寫一個函式computePrice,根據車輛租金價格、租賃日期,租賃的位置,或者客戶本身(因為它可以從一定的降低中受益)計算。很明顯這個方法應該是領域層的一部分,那麼我們應該在什麼型別的物件中放置這個方法?我們應該在Aggregate聚合?Value Object 值物件或Entity中對它進行編碼嗎?很明顯,這些物件都不適合容納computePrice這種方法。在這種情況下,最佳解決方案是將此方法儲存在專用服務類中(例如RentService),該服務類將是模型的組成部分。

領域事件
如果實體,值物件和聚合允許您在一個點上表示模型,那麼瞭解系統如何到達當前這個點通常很有用。
一個非常有用的模式可以解決這個問題:領域事件。這些是在Entity或Aggregate上對業務事務進行建模的物件。這些物件通常用於儲存,使得可以獲得模型所經歷的更改的完整歷史記錄。領域事件的另一個優點是它們簡化了系統間同步。一旦系統被修改,它就可以發出域事件以通知其他系統修改其狀態。
在實現級別,域事件是不可變物件,其包含至少一個具有事件日期的時間戳和用於標識它所在事件序列中的的序列號。單個時間戳是無法識別域事件,因為可能會有相同的毫秒中的幾個事件,尤其是在分散式系統
在銀行業,域事件的典型例子是實體 “銀行賬戶” 的“應收賬款”和“借方賬戶”事件。
該域事件是變得越來越多地被使用,因為它的概念的基礎上的模式事件溯源,這本身就很好:用的模式架構CQRS。
注意:這種模式實際上並未在Eric Evans的原始書中定義,而是在2015年釋出的精簡版和完整版中:DDD Reference
例子
在車隊的例子中,汽車可以租用,退回,送去維修。因此,每次汽車改變狀態(例如從“租用”到“渲染”),我們可以透過型別領域事件的物件來模擬這種狀態變化。如果這個物件是持久的,我們不僅可以檢視汽車的當前狀態,還可以跟蹤汽車的所有歷史記錄。

分層架構
到目前為止看到的所有模式都可用於建模業務領域。但是,如果我們不將業務程式碼與其餘程式碼隔離開來,那麼這些模式會失去很多價值。
因此,隔離業務程式碼的方法之一是使用經典的分層架構。然後,將這些層中的一個保留到業務程式碼中就足以將業務問題與軟體的其他元件(圖形介面,資料層......)分開。
但是,如果我們保持“經典”分層架構,“業務”層將依賴於下面的層程式碼(資料儲存,基礎架構)。因此,業務程式碼將取決於專案的技術選擇,如果這些選擇發生變化,則必須進行修改。但是,這與DDD的目標之一是矛盾的,即業務程式碼僅在業務發生變化時(或者當業務的理解發生變化時)才會發生變化。為了避免這種陷阱,一個技巧是使用依賴注入技術,這樣它就是依賴於業務程式碼而不是相反的技術層。因此,業務層不依賴於任何其他層,而是依賴於它的其他層。
Eric Evans在其2003年的書中闡述了這種架構。從那時起,這種架構得到了簡化和改進,導致六邊形架構只剩下兩層:域和基礎架構。還包括的另一改進分層體系結構:在洋蔥架構促進相反,層的數量較多,欄位本身被分成幾個子層。
例子
在我們的車隊示例中,假設我們需要讓客戶知道他們的車已經準備就緒。然後,我們可以提供的介面ContactAvecLeClient將在基礎架構層中實現,幷包括向客戶端傳送電子郵件。如果明天,技術選擇發生變化,我們希望透過SMS阻止客戶,那麼改變介面的實現就足夠了,這可以在不觸及業務領域層程式碼的情況下完成。

倉儲Repostiory
如果以前的模式用於建模業務,則尚未回答一個問題:如何儲存表示業務物件的資料?
如果為了儲存/檢索 聚合,我們直接在業務程式碼中呼叫SQL查詢,我們不僅打破了業務程式碼和技術程式碼的分離,而且,我們將程式碼繫結到特定的技術,例如關聯式資料庫.
因此,Repository模式透過抽象Aggregates的儲存和恢復來解決此問題。儲存庫的介面必須獨立於技術層,並且必須具有業務意義。透過獨立於儲存的技術細節,該模式使得可以在專案期間改變儲存並且還增加業務程式碼的可測試性。
注意不要將DDD 的Repository模式與簡單的資料訪問物件(DAO)混淆。實際上,後者僅用於在資料庫中對映具有條目的物件,它們不提供與技術的獨立性,並且它們的介面通常沒有商業意義。


模組
模組是基於類的應用程式中的邏輯分組。模組是絕對的最流行的程式設計概念之一。
領域驅動設計還建議使用模組。但是在業務程式碼中,類必須按功能親和性而不是按實現細節進行分組(存在程式碼中重複一點的風險)。
用於理解最後一點的比喻是器具和工具的例子:通常,我們將叉子和刀子排列在一起,因為它們具有相似的特徵(它們用作餐具)。但刀具和鋸子並沒有放在一起,儘管它們都有鋒利的金屬刀片。
例子
在我們的汽車租賃公司的車隊管理應用程式中,我們的模組應用部門可以包含一個管理汽車的模組和另一個管理客戶的模組。實際上,這些模組在我們的領域中是有意義的:它們顯然是我們業務模式的一部分。

結論
此處介紹的所有模式都旨在促進領域模型在程式碼中的表達。確實,他們幫助:

  • 分離業務邏輯和技術邏輯(儲存庫,分層架構),
  • 組織程式碼(值物件,實體,聚合,模組,服務),
  • 考慮競爭和一致性之間的權衡(聚合,域事件),
  • 明確闡述競爭與業務邏輯(域事件)之間的關係,
  • 強制解釋程式碼中的業務概念(值物件,實體,域事件)。

所以,應用這些模式提供了一個程式碼更清晰,更有條理,更適合於分散式系統,並在其中的業務邏輯是不清楚。

相關文章