Dino Esposito: 一個領域模型的設計

作者: Dino Esposito  來源: MSDN雜志  發布時間: 2012-01-16 14:51  閱讀: 6833 次  推薦: 0   原文鏈接   [收藏]  

  英文原文:Design of a Domain Model

  最新發布的 Entity Framework 4.1 和新的 Code First 開發模式打破了服務器程序開發的基本規則:如果數據庫沒有準備就緒,不要輕舉妄動(Don’t take a single step)Code First 允許開發人員重點關注業務領域并根據“類”(class)來為該領域建模。在某種程度上, Code First 模式鼓勵在 .NET 環境中應用“領域驅動設計 (DDD) ”原則。業務領域由相互關聯的實體構成,這些實體通過屬性對外公開自己的數據,通過方法和事件對外公開自己的行為。更重要的是,每個實體都可能處于某一狀態,并且與一組動態的驗證規則相綁定。

  為實際應用場景編寫對象模型會面臨一些在演示程序和教程中沒有涉及的問題。在本文中,我將挑戰這些問題,并討論如何構建 Customer 類,我會就此簡要介紹一些設計模式和設計實踐,例如Party模式、聚合根(aggregate roots)、工廠(factories)以及代碼協定(Code Contracts)和企業庫驗證應用程序塊 (VAB) 等技術。

  有一個開源項目可以作為參考,這里討論的代碼就是其中的一小部分。 它就是由 Andrea Saltarello 創建的 Northwind Starter Kit 項目 (nsk.codeplex.com) ,該項目旨在介紹構建多層解決方案的有效實踐。

  對象模型(Object Model) vs. 領域模型(Domain Model)

  爭論是使用對象模型還是領域模型似乎沒有意義,在大多數情況下,這只是一個術語表述問題(terminology) 但準確地使用術語是確保團隊所有成員在使用特定術語時始終遵循同一概念的重要因素。

  對于軟件行業的幾乎每個人而言,對象模型是一個具有共性的并且可能相關的對象的集合。領域模型有何不同? 域模型歸根結底仍然是一個對象模型,因此,交替使用這兩個術語可能不會產生嚴重的錯誤。但在專門強調使用“領域模型”一詞時,它可能會使大家對所構建的對象的形態(shape)產生某些期望。

  領域模型的這種用法與 Martin Fowler 給出的以下定義相關:

由行為和數據組合而成的領域的對象模型。相應地,這些行為用于表達業務規則和特定的業務邏輯(請參閱 P of EAA page 116)。

An object model of the domain that incorporates both behavior and data. In turn, the behavior expresses both rules and specific logic.

  DDD 向領域模型中添加了一些實用的規則。從這個角度看,領域模型不同于對象模型,它更多推薦使用值對象(value objects)而不是基元類型(primitive types)。例如在對象模型中,一個整數可能具有多種含義,它可能表示溫度、金額、大小或數量。而在領域模型中,針對各種不同的場景會使用特定的值對象類型。

  此外,領域模型需要識別出聚合根。聚合根是一個通過組合其他實體而得到的實體。聚合根中的對象與外部沒有直接的關聯,也就是不存在這樣的用例——不經過根對象而直接使用這些對象。比如,Order 實體就是一個典型的聚合根。 Order 包含聚合的 OrderItem,而不包含 Product。 難以想象您使用一個OrderItem 而它并不來自 Order(即使這只是由specs決定的,譯者注:也就是通過規約查詢直接得到相應的OderItem)。另一方面,您很可能具有這樣一些用例,您在其中使用不涉及訂單的 Product 實體。聚合根負責維護處于有效狀態的子對象并持久化這些對象。

  最后,某些領域模型類(class)可以提供用于創建新實例的公共工廠方法,而不是構造函數。如果模型類通常是獨立的并且實際上不是層次結構的一部分,或者用于創建該類的步驟只是與客戶端相關,則可以使用普通的構造函數。但是,在使用聚合根這樣的復雜對象時,您還需要實例化之外的其他抽象級別。 DDD 引入了工廠對象(簡單一些的話,可以使用類中的工廠方法)方式,這種方式可將客戶端的需求與內部的對象及其關系和規則分離開來。可以在 An Introduction to Domain Driven Design 中找到有關 DDD 的清晰簡要的介紹。

  Party模式

  讓我們重點分析一下 Customer 類。 根據上文所述,此處是可能的簽名:

public class Customer : Organization, IAggregateRoot
{
  ...
}

  誰是您的客戶? 它是個人和/或組織? Party 模式建議您區別這兩者,并清晰地定義哪些屬性是公用的,哪些屬性僅屬于個人或組織。“代碼1”中的代碼僅針對 Person 和 Organization。您可以根據業務領域的需要,將組織細分為非盈利組織和商業公司,從而細化代碼內容。

  代碼1 基于Party模式的類

public abstract class Party
{
  public virtual String Name { get; set; }
  public virtual PostalAddress MainPostalAddress { get; set; }
}
public abstract class Person : Party
{
  public virtual String Surname { get; set; }
  public virtual DateTime BirthDate { get; set; }
  public virtual String Ssn { get; set; }
}
public abstract class Organization : Party
{
  public virtual String VatId { get; set; }
}

  您必須始終記住,您的目標是構建一個可為您的實際業務領域精確建模的模型,而不是生成該業務的抽象表示。如果您的需求只涉及作為個體的客戶(Customer),那么 Party 模式不是必需的,即使該模式帶來了后續可擴展性。

  作為聚合根的Customer類

  聚合根是模型中的一個類,它表示一個獨立的實體——在與其他實體的關系中并不存在(one that doesn’t exist in relation to other entities,譯者注:也就是與其他實體不存在關聯)。在大多數情況下,您的聚合根只是單獨的類,這些類不管理任何子對象,或者只是指向其他聚合的根。 “代碼2顯示了更詳細的 Customer 類。

  代碼2 作為聚合根的 Customer 類

public class Customer : Organization, IAggregateRoot
{
  public static Customer CreateNewCustomer(
    String id, String companyName, String contactName)
  {
    ...
  }
 
  protected Customer()
  {
  }
 
  public virtual String Id { get; set; }
    ...
  public virtual IEnumerable<Order> Orders
  {
    get { return _Orders; }
  }
   
  Boolean IAggregateRoot.CanBeSaved
  {
    get { return IsValidForRegistration; }
  }
 
  Boolean IAggregateRoot.CanBeDeleted
  {
    get { return true; }
  }
}

  正如您所看到的,Customer 類實現了(自定義)IAggregateRoot 接口。 代碼如下

public interface IAggregateRoot
{
  Boolean CanBeSaved { get; }
  Boolean CanBeDeleted { get; }
}

  成為聚合根意味著什么? 聚合根處理所包含的子聚合對象的持久化,并負責強制實施與該組對象相關的不變條件( invariant conditions)。因此,聚合根應該能夠檢查整個聚合對象堆(stack)是否能被保存或刪除。獨立聚合根只返回 True,而不進行任何進一步檢查。

  工廠和構造函數

  構造函數是特定于類型的。如果對象只是一個類型(沒有聚合并且沒有復雜的初始化邏輯),那么使用普通的構造函數會更好。工廠通常是一個有用的額外抽象層。工廠可以是實體類中的一個簡單的靜態方法,也可以是一個單獨的組件。使用工廠方法還可以讓代碼更具可讀性,因為通過它你可以清楚地知道為何要這樣實例化。如果使用構造函數,那么您在處理不同實例化場景時將受到更多的限制,因為構造函數的方法名不能隨意更改(只能與類同名),只能通過簽名來識別它。特別是長簽名(有很多參數的構造函數),在以后使用時會很難弄明白為什么要這樣實例化。 “代碼3”顯示了 Customer 類中的工廠方法。

  代碼3 Customer 類中的工廠方法

public static Customer CreateNewCustomer(
  String id, String companyName, String contactName)
{
  Contract.Requires<ArgumentNullException>(
           id != null, "id");
  Contract.Requires<ArgumentException>(
           !String.IsNullOrWhiteSpace(id), "id");
  Contract.Requires<ArgumentNullException>(
           companyName != null, "companyName");
  Contract.Requires<ArgumentException>(
           !String.IsNullOrWhiteSpace(companyName), "companyName");
  Contract.Requires<ArgumentNullException>(
           contactName != null, "contactName");               
  Contract.Requires<ArgumentException>(
           !String.IsNullOrWhiteSpace(contactName), "contactName");
 
  var c = new Customer
              {
                Id = id,
                Name = companyName,
                Orders = new List<Order>(),
                ContactInfo = new ContactInfo
                              {
                                 ContactName = contactName
                              }
              };
  return c;
}

  工廠方法是一個原子操作,可獲取輸入參數、執行其作業并返回指定類型的新實例。應確保返回的實例處于有效狀態。工廠負責履行所有已定義的內部驗證規則。

  工廠還需要驗證輸入數據。為此,可使用代碼協定(Code Contracts)前提條件來保證代碼的清晰和高可讀性。還可以使用后置條件來確保返回的實例處于有效狀態,如下所示:

Contract.Ensures(Contract.Result<Customer>().IsValid()); 

  如果在整個類中使用不變式(invariants),經驗表明,您無法始終提供這些不變式。不變式的侵入性可能太強,特別是在復雜的大型模型中。代碼協定(Code Contracts)不變式有時可能過于嚴格地遵循規則集,而在您的代碼中,有時需要更多的靈活性。因此,最好對必須強制執行不變式的區域進行限制。

  驗證

  可能需要驗證領域類中的屬性,以確保必填字段不為空,文本沒有超出長度限制,并且相關數值處于規定的范圍內等等。您還必須考慮進行跨屬性驗證以及復雜的業務規則。如何進行代碼驗證?

  驗證涉及條件代碼,最終涉及組合某些 if 語句,并返回布爾值。不借助任何框架或技術,純手工編寫驗證層也許可行,但實際上并不是一個好主意。這樣編寫出來的代碼的可讀性和后續改進的方便性得不到保證,通過一些流暢的代碼工具庫(fluent libraries)可以改善這種情況。受實際業務規則的限制,驗證規則可能會經常變化,您的實現必須考慮到這一點。因此,您不能只編寫針對當前驗證規則的代碼,而是應該編寫能夠適應驗證規則變化的更靈活的代碼。

  在驗證過程中,有時您希望傳入無效數據時給出提示,有時您只希望收集相關錯誤并將其報告給其他代碼層。記住,代碼協定不參與驗證過程,它只檢查各種條件,然后在條件不適用時引發異常。通過集中式錯誤處理程序,您可以從異常中進行恢復并妥善降級。通常建議僅在領域實體中使用代碼協定,以便捕獲可能導致出現不一致狀態的潛在嚴重錯誤。也可以在工廠中使用代碼協定,在這種情況下,如果傳入的數據無效,代碼必須引發異常。是否在屬性的 setter 方法中使用代碼協定由您自己決定。我更喜歡采用更舒適的方式,通過特性類(Attribute)進行驗證。但使用哪些Attribute呢?

  Data Annotations(數據注解)與企業庫VAB(Enterprise Library Validation Enterprise Block)

  Data Annotations 命名空間和企業庫 VAB 非常類似。這兩種框架均基于Attribute,可以使用表示自定義規則的自定義類對其進行擴展。在這兩種情況下,您都可以定義跨屬性(property)驗證。最后,這兩種框架都提供了驗證API,用于評估實例并返回錯誤列表。這兩者有何區別?

  Data Annotations 是 Microsoft .NET Framework 的一部分,不需要單獨下載。企業庫需要單獨下載,在大型項目中并不重要,但在企業應用中可能需要批準,因此仍會產生問題。可以通過 NuGet 輕松安裝企業庫(請參閱本期專欄中的“使用 NuGet 管理項目庫”一文)。

  企業庫 VAB 在以下方面優于Data Annotations:可以通過 XML 規則集對其進行配置。XML 規則集是您用于描述所需驗證的配置文件中的條目。不用說,您能夠以聲明方式更改某些內容,甚至無需改動代碼。 “代碼4”顯示了一個示例規則集。

  代碼4 企業庫規則集

<validation>
   <type assemblyName="..." name="ValidModel1.Domain.Customer">
     <ruleset name="IsValidForRegistration">
       <properties>
         <property name="CompanyName">
           <validator negated="false"
                      messageTemplate="The company name cannot be null" 
                      type="NotNullValidator" />
           <validator lowerBound="6" lowerBoundType="Ignore"
                      upperBound="40" upperBoundType="Inclusive" 
                      negated="false"
                      messageTemplate="Company name cannot be longer ..."
                      type="StringLengthValidator" />
         </property>
         <property name="Id">
           <validator negated="false"
                      messageTemplate="The customer ID cannot be null"
                      type="NotNullValidator" />
         </property>
         <property name="PhoneNumber">
           <validator negated="false"
                      type="NotNullValidator" />
           <validator lowerBound="0" lowerBoundType="Ignore"
                      upperBound="24" upperBoundType="Inclusive"
                      negated="false"
                      type="StringLengthValidator" />
         </property>
         <property name="FaxNumber">
           <validator negated="false"
                      type="NotNullValidator" />
           <validator lowerBound="0" lowerBoundType="Ignore"
                      upperBound="24" upperBoundType="Inclusive"
                      negated="false"
                      type="StringLengthValidator" />
         </property>
       </properties>
     </ruleset>
   </type>
 </validation>

  規則集列出了您要應用于指定類型中的指定屬性 (Property) 的Attribute。在代碼中,您可以按如下所示驗證規則集:

public virtual ValidationResults ValidateForRegistration()
{
  var validator = ValidationFactory
          .CreateValidator<Customer>("IsValidForRegistration");
  var results = validator.Validate(this);
  return results;
}

  該方法將 IsValidForRegistration 規則集中列出的驗證程序應用于指定實例。

  關于驗證和庫的最后一點說明。我在這里沒有談及每個常用的驗證庫,但它們之間并沒有明顯的區別。重要的是考慮您的業務規則是否發生了更改及更改頻率如何。您可以在此基礎上決定是Data Annotations、Enterprise Library VBA、代碼約定(Code Contracts)還是其他某個庫更合適。根據我的經驗,如果您確切知道所需實現的目標,則可以輕松地選擇“正確”的驗證庫。

  總結

  用于真實業務領域的對象模型幾乎不會僅僅是屬性和類的簡單集合。此外,在考慮技術問題之前應先考慮設計方面的事項。一個設計完好的對象模型可以表達該領域所需的方方面面。在大多數時候,這代表可以輕松地對“類”進行初始化和驗證,可以方便地給“類”增加更多的屬性與邏輯。不應該教條式地看待 DDD 實踐,它應該成為你明確前進方向的指南。

  Dino Esposito 是《Programming Microsoft ASP.NET 4》(Microsoft Press,2011 年)和《Programming Microsoft ASP.NET MVC》(Microsoft Press,2011 年)的作者,同時也是《Microsoft .NET:Architecting Applications for the Enterprise》(Microsoft Press,2008 年)的合著者。 Esposito 定居于意大利,經常在世界各地的業內活動中發表演講。 有關他的情況,請訪問 Twitter 上的 twitter.com/despos

0
0
 
標簽:DDD
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(1) 人氣()