本文章是由機器翻譯。
資料點
針對網域導向設計撰寫程式碼:進行資料型開發的秘訣,第 2 篇
本月的專欄將繼續慶祝十周年的EricEvans 的書,"域驅動的設計:在軟體核心複雜性應對之道"(艾迪生-衛斯理專業,2003年)。我要為數據-第一次開發人員感興趣的一些編碼模式的 Domain-Driven 設計 (DDD) 受益于分享的更多提示。突出顯示的最後一個月的列:
- 設置持久性問題放在一邊當建模域
- 公開推動您的實體和集料,但不是你的屬性 setter 方法
- 認識到某些子系統完美的簡單的創建、 讀取、 更新和刪除 (CRUD) 互動和不需要域建模
- 效益的不嘗試跨界上下文共用資料或類型
在本專欄中,您將學習什麼是"貧血"條款和"富"的域模型,以及的值物件的概念。值物件似乎是一個主題,為開發人員分為兩個陣營。它是如此的明顯有些讀者不能想像為什麼值得任何討論,或如此令人費解的是別人從未能夠在它附近在他們腦袋。我還會鼓勵你考慮在某些方案中使用的值物件相關的物件位置。
那那種居高臨下的期限:貧血的域模型
有一副你會經常聽到關於如何在 DDD 中定義的類的條款 — — 貧血域模型和富域模型。在 DDD 中,域模型是指一類。富域模型是一個與 DDD 辦法相符 — — 類 (或類型),用行為,不只是與 getter 和 setter 定義的。相比之下,貧血域模型包括簡單的 getter 和 setter (和也許幾種簡單的方法),這是許多方案中,但從 DDD 收益沒有任何好處。
當使用Entity Framework(EF) 代碼第一次的啟動時,我打算的代碼先與工作的類可能是貧血的 99%。圖 1 是一個完美的例子,顯示一種客戶類型和它從中繼承的類型的人。我經常會將包括少量的方法,但基本類型僅僅具有 getter 和 setter 的架構。
圖 1 典型貧血域模型的類看起來像資料庫表
public class Customer : Person
{
public Customer()
{
Orders = new List<Order>();
}
public ICollection<Order> Orders { get; set; }
public string SalesPersonId { get; set; }
public ShippingAddress ShippingAddress { get; set; }
}
public abstract class Person
{
public int Id { get; set; }
public string Title { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string CompanyName { get; set; }
public string EmailAddress { get; set; }
public string Phone { get; set; }
}
我終於看著這些類,並意識到我在做小多定義一個資料庫中表我的課。 它不一定是件壞事,你打算如何處理類型。
相比,在列出的富裕客戶類型圖 2,這是我在我以前的專欄中探討客戶類型相似 (msdn.microsoft.com/magazine/dn342868)。 它公開控制訪問的屬性和其他類型的聚合的一部分的方法。 因為你看到它要更好地表達的一些主題我討論這個月的早些時候列在調整了一下的客戶類型。
圖 2 是一個富域模型,不只是屬性的客戶類型
public class Customer : Contact
{
public Customer(string firstName, string lastName, string email)
{
FullName = new FullName(firstName, lastName);
EmailAddress = email;
Status = CustomerStatus.Silver;
}
internal Customer()
{
}
public void UseBillingAddressForShippingAddress()
{
ShippingAddress = new Address(
BillingAddress.Street1, BillingAddress.Street2,
BillingAddress.City, BillingAddress.Region,
BillingAddress.Country, BillingAddress.PostalCode);
}
public void CreateNewShippingAddress(string street1, string street2,
string city, string region, string country, string postalCode)
{
BillingAddress = new Address(
street1,street2,
city,region,
country,postalCode)
}
public void CreateBillingInformation(string street1,string street2,
string city,string region,string country, string postalCode,
string creditcardNumber, string bankName)
{
BillingAddress = new Address (street1,street2, city,region,country,postalCode );
CreditCard = new CustomerCreditCard (bankName, creditcardNumber );
}
public void SetCustomerContactDetails
(string email, string phone, string companyName)
{
EmailAddress = email;
Phone = phone;
CompanyName = companyName;
}
public string SalesPersonId { get; private set; }
public CustomerStatus Status { get; private set; }
public Address ShippingAddress { get; private set; }
public Address BillingAddress { get; private set; }
public CustomerCreditCard CreditCard { get; private set; }
}
在此更豐富的模型,而不只暴露要讀取和寫入的屬性,公共表面的客戶組成的顯式方法。 查看更多詳細資訊的最後一個月的列的私營 Setter 和公共方法部分。 我的觀點是,説明你更好地理解 DDD 所指作為貧血與富域模型之間的差異。
值物件可能會造成混淆
雖然它們看起來簡單,DDD 值物件是困惑的一個嚴重的許多人,包括我。 我已經閱讀並聽取了很多不同的方式描述值物件從不同的角度。 幸運的是,每個不同的解釋,而不是衝突與另一個,幫我生成值物件更深入的認識。
在其核心,值物件是一個類,沒有標識鍵。
Entity Framework具有"複雜類型"的概念來相當密切。 複雜類型不具有標識鍵 — — 他們將封裝一組屬性的方式。 EF 理解如何處理這些物件,當它來到資料持久性。 在資料庫中,他們得到存儲為該實體映射到表的欄位。
例如,一個名字屬性和姓氏屬性在您的類的人,而你可能有 FullName 屬性:
public FullName FullName { get; set; }
FullName 可以封裝的名字和姓氏的屬性,與部隊你才能同時提供這些屬性的建構函式的類。
但一個值物件是更多的複雜類型。 在 DDD 中,有三個值物件的定義特徵:
- 它有沒有標識鍵 (這與複雜類型對齊)
- 它是不可變
- 在檢查其平等到其他實例相同的類型時,它的所有值進行比較
有趣的是,不變性。 沒有身份金鑰,一種類型的不可變性定義其身份。 你總是可以通過其屬性的組合標識實例。 因為它是不可變的沒有任何類型的屬性可以更改,所以它是安全地使用該類型的值來確定特定的實例。 (你已經看到如何 DDD 實體保護自己免受隨機修改通過使其 setter 私有的但你可以有一種方法使您可以影響這些屬性。不僅值物件不會隱藏 setter,它阻止你修改的任何屬性。 如果您修改了某個屬性,這意味著該物件的值已更改。 因為整個物件表示它的值,您不要單獨與它的屬性交互。 如果您需要的物件要有不同的值,您創建物件來保存一組新的值的一個新的實例。 在結束時,有沒有這種東西作為更改的值物件的一個屬性 — — 有效地使得即使制訂一個句子,其中包括一句"如果你需要更改屬性的值的"矛盾修飾法。 一種有用的並行是要考慮,這是另一個不可變類型的字串 (至少在所有的語言,我熟悉)。 使用一個字串實例時,您不要替換單個字元的字串。 您只需創建一個新的字串。
我 FullName 值物件所示圖 3。 它有沒有身份的關鍵屬性。 您可以看到其具現化的唯一方法是通過提供這兩個屬性值,並沒有任何方法可以修改這些屬性之一。 所以它滿足的不變性要求。 最後一項要求,它提供的方法來比較平等的另一個該類型的實例,在自訂的 ValueObject 類中隱藏 (,我借了吉米博加德在從 bit.ly/13SWd9h) 從它繼承的因為那是一群令人費解的代碼。 雖然此 ValueObject 並沒有考慮集合的屬性,它滿足我的需求,因為我沒有任何中此值物件的集合中。
圖 3 FullName 值物件
public class FullName:ValueObject<FullName>
{
public FullName(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public FullName(FullName fullName)
: this(fullName.FirstName, fullName.LastName)
{ }
internal FullName() { }
public string FirstName { get; private set; }
public string LastName { get; private set; }
// More methods, properties for display formatting
}
牢記您可能需要修改過的副本 — — 例如,類似于此 FullName 但有不同的名字 — — 沃恩弗農,"執行 Domain-Driven 設計"(艾迪生-衛斯理專業、 2013年),作者表明 FullName 可能包括從現有實例創建新實例的方法:
public FullName WithChangedFirstName(string firstName)
{
return new FullName(firstName, this.LastName);
}
public FullName WithChangedLastName(string lastName)
{
return new FullName(this.FirstName, lastName);
}
當我在我持久層 (Entity Framework) 中添加時,它會認為這是它使用任何類的 EF 複雜類型屬性。 當我使用 FullName 的我喜歡的人的類型的屬性時,EF 將人類型的存儲位置的資料庫表中存儲 FullName 的屬性。 預設情況下,屬性將命名 FullName_FirstName 和 FullName_LastName 在資料庫中。
FullName 是相當簡單的。 即使你在做資料庫集中設計,你可能不想要存儲在一個單獨的表中的名字和姓氏。
值物件或相關的物件嗎?
但現在考慮另一種情況 — — 想像一下 ShippingAddress 和 BillingAddress 的客戶:
public Address ShippingAddress { get; private set; }
public Address BillingAddress { get; private set; }
(資料驅動大腦) 預設情況下,作為一個實體,包括一個 AddressId 屬性創建的位址。 而且,再次,因為我"認為在資料中,"我猜將作為一個單獨的表在資料庫中存儲的位址。 好的我努力訓練自己停在建模我的域,這就是沒有後果的同時考慮資料庫。 然而,有些時候我會在我的資料中添加圖層使用 EF 和 EF 亦會作出這種假設。 但是 EF 不能映射出正常工作。 它將假定 0..1: * 位址與客戶 ; 之間的關係 換句話說,位址可能有任意數量的相關的客戶和客戶將有沒有位址或一個位址。
我的資料庫管理員可能不喜歡這樣的結果,更重要的是,我不會寫我的應用程式代碼基於同樣的假設,Entity Framework是 — — 有很多客戶到零個或一個位址。 因此,EF 可能會影響我的資料持久性或檢索在一些意想不到的方式。 所以一直與 EF 有很多的人的角度看,從我第一種辦法是修復使用 Fluent API 的 EF 映射。 但問 EF 要修復此問題將迫使我要從位址重新指向客戶,我不想在我的模型添加導覽屬性。 正如您所看到的解決這一問題與 EF 映射會引領我走一個兔子洞。
當我退後一步,專注于域,不能基於 EF 或資料庫中,更有意義,只是使位址而不是一個實體的值物件。 三個步驟是必需的:
- 我需要從 (可能稱為 Id 或 AddressId) 的網址類別型中刪除的鍵屬性。
- 我需要確保位址是不可變的。 其建構函式已經讓我填充的所有欄位。 若要刪除任何方法,將允許任何屬性,以改變了
- 我需要確保平等基於其屬性和欄位的值,就可以檢查位址。 我可以做到通過繼承再次從我從博加德吉米借那好 ValueObject 類 (請參閱圖 4)。
圖 4 位址作為一個值物件
public class Address:ValueObject<Address>
{
public Address(string street1, string street2,
string city, string region,
string country, string postalCode) {
Street1 = street1;
Street2 = street2;
City = city;
Region = region;
Country = country;
PostalCode = postalCode; }
internal Address() { }
public string Street1 { get; private set; }
public string Street2 { get; private set; }
public string City { get; private set; }
public string Region { get; private set; }
public string Country { get; private set; }
public string PostalCode { get; private set; }
// ...
}
我如何從我的客戶類使用此網址類別不會改變,只是如果我需要修改的位址,我會創建一個新實例。
但現在不再需要擔心管理這種關係。 而且它還提供另一個試金石為值物件,在這一個客戶真的由其航運定義和帳單資訊,因為如果我賣任何東西給該客戶,我最有可能需要船舶的它和帳款部需要有其帳單郵件標籤。
這並不是說每一個 1:1 或 1:0..1 關係可以替換值物件,但它是一個漂亮的解決方案和我的工作成為了當我停止不必後突然出現了,當我試圖迫使Entity Framework,以維持這種關係對我的另一種解決一個難題的要簡單得多。
像 FullName 示例中,更改位址的值的物件是指Entity Framework將看到位址作為複雜類型,並將存儲所有客戶表中的資料。 年的重點是資料庫正常化使我自動認為這是一件壞事,但我的資料庫可以輕鬆地處理它和它為我特定域工作。
我能想到的很多論據反對使用這種技術,但他們都以"如果怎樣"開頭那些不屬於我的域的任何不是有效的參數。 我們浪費了很多時間編碼為永遠不會出現的情況只是方案。 我想得更周到有關添加那些搶先創口貼到我的解決方案。
相當尚未完成
我越來越困擾這資料怪人的 DDD 概念的清單中。 我大敢相信更多的這些概念,我成為學習更多的靈感。 一旦我的思想有點麼這些模式為我做很有意義。 他們不減少資料持久性的一位,但分離的域從資料持久性和下文我興趣結構權生他們一起複雜了很多很多年後我的感覺。 儘管如此,它是重要的是要記住有很多不需要 DDD 的軟體活動。 DDD 可以説明理清複雜的問題,但很多時候它的為簡單的矯枉過正。
在下一列中我將討論幾個其他 DDD 技術戰略,起初也似乎與我的資料第一的思想,如放手的雙向關係,考慮不變數和處理任何認為需要觸發來自您的聚合資料存取違規。
Julie Lerman 是 Microsoft MVP,.NET 的導師和諮詢師,住在佛蒙特州的山裡。你可以找到她提出關於資料訪問和使用者組和會議,世界各地其他 Microsoft.NET 主題。在她博客 thedatafarm.com/blog 和的作者是"程式設計Entity Framework"(2010 年) 以及代碼第一版 (2011 年) 和 DbCoNtext 版 (2012 年),所有從 O'Reilly 媒體。跟著她在 Twitter 上 twitter.com/julielerman 看看她的 Pluralsight 課程 juliel.me/PS-視頻。
由於以下的技術專家對本文的審閱:Stephen倫 (Microsoft)
StephenA. 倫目前是微軟公司高級技術顧問,並帶來他多樣 20-加-年的經驗作為實踐建築師、 CAD 經理、 IT 技術員、 軟體工程師、 首席技術官、 和顧問協助選擇 Microsoft 合作夥伴組織在其通過 Microsoft 開發者產品和技術最前沿和預發行前。