爲每個業務微服務或綁定上下文定義一個豐富域模型。
你的目標是爲每個業務微服務或綁定上下文 (BC) 創建一個內聚域模型。 但請記住,BC 或業務微服務有時可能由共享一個域模型的多個物理服務組成。 域模型必須捕獲它所代表的單個綁定上下文或業務微服務的規則、行爲、業務語言和約束。
域實體模式
實體表示域對象,主要由其標識、連續性和隨時間推移的持久性來定義,而不僅僅由構成它們的屬性來定義。 正如 Eric Evans 所說,“主要由其標識定義的對象稱爲實體”。 實體在域模型中非常重要,因爲它們是模型的基礎。 因此,應對其進行仔細識別和設計。
實體的標識可以跨多個微服務或綁定上下文。
同一標識(即,同一 Id
,不過可能不是同一域實體)可以跨多個綁定上下文或微服務建模。不過,這並不意味着具有相同屬性和邏輯的相同實體會在多個綁定上下文中實現。 相反,每個綁定上下文中的實體會將其屬性和行爲限制爲該綁定上下文域中所需的屬性和行爲。
例如,買家實體可能具有某個人的大部分屬性,這些屬性在配置文件或標識微服務的用戶實體中定義,其中包括標識。 但是訂購微服務中的買家實體可能具有較少的屬性,因爲只有某些買家數據與訂單流程相關。 每個微服務的上下文或每個綁定上下文都會影響其域模型。
除了實現數據屬性外,域實體還必須實現行爲。
DDD 中的域實體必須實現與實體數據(在內存中訪問的對象)相關的域邏輯或行爲。 例如,作爲訂單實體類的一部分,你必須將業務邏輯和操作作爲任務(例如添加訂單項、數據驗證和總計算)的方法實現。 實體的方法負責處理實體的不變量和規則,而不是將這些規則分佈在應用層中。
圖 7-8 展示了一個域實體,它不僅實現數據屬性,還實現具有相關域邏輯的操作或方法。
圖 7-8。 實現數據加行爲的域實體設計示例
當然,實體有時可能不會在實體類中實現任何邏輯。 如果某個聚合內的子實體沒有任何特殊邏輯,因爲大多數邏輯都在聚合根中定義,則該子實體可能出現這種情況。 如果你有一個複雜的微服務,它在服務類而非域實體中實現了大量邏輯,那麼你可能會陷入貧乏域模型中,下一節將對此進行解釋。
豐富域模型與貧乏域模型
Martin Fowler 在他的博客文章 AnemicDomainModel 中是這樣描述貧乏域模型的:
貧乏域模型的基本症狀是,乍一看上去像是真實存在的。 它包含一些對象,許多以域空間中的名詞命名,這些對象與真實域模型具有的豐富關係和結構相關聯。 但當你觀察它的行爲時,問題來了,你發現這些對象幾乎沒有任何行爲,完全就是一些 getter 和 setter 而已。
當然,使用貧乏域模型時,將從一組可捕獲所有域或業務邏輯的服務對象(傳統上稱爲業務層)中使用這些數據模型。 業務層位於數據模型之上,就像使用數據一樣使用數據模型。
貧乏域模型就是一種程序化樣式設計。 貧乏實體對象不是真實的對象,因爲它們缺乏行爲(方法)。 它們只保存數據屬性,因此它不是一種面向對象的設計。 通過將所有行爲放到服務對象(業務層)中,實質上最終會產生麪條式代碼或事務腳本,因而失去域模型提供的優勢。
不管怎樣,如果微服務或綁定上下文非常簡單(CRUD 服務),僅包含數據屬性的實體對象形式的貧乏域模型可能已經足夠,沒必要實現更復雜的 DDD 模式。 在這種情況下,它就是一個持久性模型,因爲你特意創建了一個僅包含用於 CRUD 的數據的實體。
這就是爲什麼微服務體系結構特別適用於多體系結構方法(具體取決於每個綁定上下文)。 例如,在 eShopOnContainers 中,訂購微服務實現了 DDD 模式,但目錄微服務(一種簡單的 CRUD 服務)沒有。
有人說貧乏域模型是一種反模式。 這真的取決於你要實現什麼。 如果你創建的微服務足夠簡單(例如,CRUD 服務),則採用貧乏域模型,它不是反模式。 但是,如果需要解決包含大量不斷變化的業務規則的微服務域的複雜性,那麼貧乏域模型可能是該微服務或綁定上下文的反模式。 在這種情況下,將其設計爲具有包含數據加行爲的實體的豐富模型並實現附加 DDD 模式(聚合、值對象等)可能對這種微服務的長期成功具有極大的好處。
其他資源
-
DevIQ.Domain Entity \(域實體) https://deviq.com/entity/
-
Martin Fowler。The Domain Model \(域模型)https://martinfowler.com/eaaCatalog/domainModel.html
-
Martin Fowler。The Anemic Domain Model \(貧乏域模型)https://martinfowler.com/bliki/AnemicDomainModel.html
值對象模式
正如 Eric Evans 所指出的,“許多對象沒有概念標識。 這些對象用於描述某個事物的某些特徵。”
實體需要標識,但系統中的許多對象不需要,例如值對象模式。 值對象是一種沒有概念標識的對象,用於描述域方面。 這些對象實例化後可表示設計元素,你只會暫時關注它們。 你只關心它們是什麼,而不關心它們是誰。 其示例包括數字和字符串,但也可以是更高級別的概念,例如屬性組。
一個微服務中的實體在另一個微服務中可能就不是實體,因爲在第二種情況下,綁定上下文可能具有不同的含義。 例如,電子商務應用程序中的地址可能根本沒有標識,因爲它可能僅表示個人或公司的客戶資料的一組屬性。 在這種情況下,地址應歸類爲值對象。 但是,在電力公司的應用程序中,客戶地址對於業務領域可能很重要。 因此,該地址必須具有標識,以便計費系統直接鏈接到該地址。 在這種情況下,地址應歸類爲域實體。
有名字和姓氏的人通常是一個實體,因爲這個人具有標識,即使其名字和姓氏與另一組值重合(例如這些名字還指另一個人)也是如此。
值對象在關係數據庫和 ORM(如 EF)中很難管理,而在面向文檔的數據庫中,它們更易於實現和使用。
EF Core 2.0 包含實體功能,這樣可以更易於處理值對象,如我們稍後詳細介紹的一樣。
其他資源
-
Martin Fowler。Value Object pattern \(值對象模式)https://martinfowler.com/bliki/ValueObject.html
-
Value Object \(值對象) https://deviq.com/value-object/
-
Value Objects in Test-Driven Development \(測試驅動開發中的值對象)https://leanpub.com/tdd-ebook/read#leanpub-auto-value-objects
-
Eric Evans。Domain-Driven Design:Tackling Complexity in the Heart of Software.(域驅動設計:軟件核心複雜性應對之道) (書;包括值對象的討論)\https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/
聚合模式
域模型包含不同數據實體和進程的羣集,可以控制功能的重要方面,例如訂單履行或庫存。 聚合是一種粒度更細的 DDD 單元,用於描述一羣或一組可視爲內聚單元的實體和行爲。
通常基於所需事務來定義聚合。 一個典型的例子就是訂單,訂單中還包含訂單項列表。 訂單項通常是一個實體。 但它是訂單聚合內的子實體,該聚合還包含訂單實體作爲其根實體,通常稱爲聚合根。
識別聚合可能很難。 聚合是一組必須一致的對象,但不能按此挑選一組對象就將它們標記爲聚合。 你必須從域概念開始,並考慮在與該概念相關的最常見事務中使用的實體。 那些需要在事務上一致的實體就形成一個聚合。 考慮事務操作可能是識別聚合的最佳方式。
聚合根或根實體模式
聚合由至少一個實體組成:聚合根,也稱爲根實體或主實體。 此外,它還可以有多個子實體和值對象,所有實體和對象一起共同實現所需的行爲和事務。
聚合根的目的是確保聚合的一致性;它應該是通過聚合根類中的方法或操作更新聚合的唯一入口點。 只能通過聚合根對聚合內的實體進行更改。 它是聚合的一致性守護者,它會考慮到可能需要在聚合中遵守的所有不變量和一致性規則。 如果單獨更改某個子實體或值對象,聚合根無法確保聚合處於有效狀態。 這就像一張桌腳鬆動了的桌子。 保持一致性是聚合根的主要目的。
在圖 7-9 中,可以看到一些示例聚合,例如買家聚合,其中包含一個實體(聚合根 Buyer)。訂單聚合包含多個實體和一個值對象。
圖 7-9。 包含多個或單個實體的聚合示例
請注意,視你的域而定,Buyer 聚合可能會有其他子實體,就像在 eShopOnContainers 參考應用程序的訂購微服務中那樣。 圖 7-9 僅列舉了買傢俱有單個實體的情況,作爲僅包含聚合根的聚合示例。
爲了讓聚合一直相互隔離並保持它們之間的清晰界限,建議禁止在 DDD 域模型中的聚合之間直接導航,並且模型僅具有外鍵 (FK) 字段,正如在 eShopOnContainers 的訂購微服務域模型中所實現的那樣。 Order 實體針對買家只有 FK 字段,沒有 EF Core 導航屬性,如以下代碼所示:
C#複製
public class Order : Entity, IAggregateRoot
{
private DateTime _orderDate;
public Address Address { get; private set; }
private int? _buyerId; //FK pointing to a different aggregate root
public OrderStatus OrderStatus { get; private set; }
private readonly List<OrderItem> _orderItems;
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;
// ... Additional code
}
識別和使用聚合需要經過大量探索與體會。 有關詳細信息,請參閱下面的“其他資源”列表。
其他資源
-
Vaughn Vernon。Effective Aggregate Design - Part I:Modeling a Single Aggregate(來自 http://dddcommunity.org/)\(有效的聚合設計 - 第 1 部分:單個聚合建模) http://dddcommunity.org/wp-content/uploads/files/pdf_articles/Vernon_2011_1.pdf
-
Vaughn Vernon。Effective Aggregate Design - Part II:Making Aggregates Work Together(來自 http://dddcommunity.org/)\(有效的聚合設計 - 第 2 部分:讓聚合共同工作) http://dddcommunity.org/wp-content/uploads/files/pdf_articles/Vernon_2011_2.pdf
-
Vaughn Vernon。Effective Aggregate Design - Part III:Gaining Insight Through Discovery(來自 http://dddcommunity.org/)\(有效的聚合設計 - 第 3 部分:通過發現獲取見解) http://dddcommunity.org/wp-content/uploads/files/pdf_articles/Vernon_2011_3.pdf
-
Sergey Grybniak。DDD Tactical Design Patterns \(DDD 戰術設計模式)https://www.codeproject.com/Articles/1164363/Domain-Driven-Design-Tactical-Design-Patterns-Part
-
Chris Richardson.Developing Transactional Microservices Using Aggregates \(使用聚合開發事務微服務) https://www.infoq.com/articles/microservices-aggregates-events-cqrs-part-1-richardson
-
DevIQ.The Aggregate pattern \(聚合模式) https://deviq.com/aggregate-pattern/