咨詢電話:023-6276-4481
熱門文章
電 話:023-6276-4481
郵箱:broiling@qq.com
地址:重慶市南岸區(qū)亞太商谷6幢25-2
最新發(fā)布的 Entity Framework 4.1 和新的 Code First 開發(fā)模式打破了服務(wù)器程序開發(fā)的基本規(guī)則:如果數(shù)據(jù)庫沒有準(zhǔn)備就緒,不要輕舉妄動(Don’t take a single step)。Code First 允許開發(fā)人員重點關(guān)注業(yè)務(wù)領(lǐng)域并根據(jù)“類”(class)來為該領(lǐng)域建模。在某種程度上, Code First 模式鼓勵在 .NET 環(huán)境中應(yīng)用“領(lǐng)域驅(qū)動設(shè)計 (DDD) ”原則。業(yè)務(wù)領(lǐng)域由相互關(guān)聯(lián)的實體構(gòu)成,這些實體通過屬性對外公開自己的數(shù)據(jù),通過方法和事件對外公開自己的行為。更重要的是,每個實體都可能處于某一狀態(tài),并且與一組動態(tài)的驗證規(guī)則相綁定。
為實際應(yīng)用場景編寫對象模型會面臨一些在演示程序和教程中沒有涉及的問題。在本文中,我將挑戰(zhàn)這些問題,并討論如何構(gòu)建 Customer 類,我會就此簡要介紹一些設(shè)計模式和設(shè)計實踐,例如Party模式、聚合根(aggregate roots)、工廠(factories)以及代碼協(xié)定(Code Contracts)和企業(yè)庫驗證應(yīng)用程序塊 (VAB) 等技術(shù)。
有一個開源項目可以作為參考,這里討論的代碼就是其中的一小部分。 它就是由 Andrea Saltarello 創(chuàng)建的 Northwind Starter Kit 項目 (nsk.codeplex.com) ,該項目旨在介紹構(gòu)建多層解決方案的有效實踐。
爭論是使用對象模型還是領(lǐng)域模型似乎沒有意義,在大多數(shù)情況下,這只是一個術(shù)語表述問題(terminology)。 但準(zhǔn)確地使用術(shù)語是確保團(tuán)隊所有成員在使用特定術(shù)語時始終遵循同一概念的重要因素。
對于軟件行業(yè)的幾乎每個人而言,對象模型是一個具有共性的并且可能相關(guān)的對象的集合。領(lǐng)域模型有何不同? 域模型歸根結(jié)底仍然是一個對象模型,因此,交替使用這兩個術(shù)語可能不會產(chǎn)生嚴(yán)重的錯誤。但在專門強調(diào)使用“領(lǐng)域模型”一詞時,它可能會使大家對所構(gòu)建的對象的形態(tài)(shape)產(chǎn)生某些期望。
領(lǐng)域模型的這種用法與 Martin Fowler 給出的以下定義相關(guān):
由行為和數(shù)據(jù)組合而成的領(lǐng)域的對象模型。相應(yīng)地,這些行為用于表達(dá)業(yè)務(wù)規(guī)則和特定的業(yè)務(wù)邏輯(請參閱 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 向領(lǐng)域模型中添加了一些實用的規(guī)則。從這個角度看,領(lǐng)域模型不同于對象模型,它更多推薦使用值對象(value objects)而不是基元類型(primitive types)。例如在對象模型中,一個整數(shù)可能具有多種含義,它可能表示溫度、金額、大小或數(shù)量。而在領(lǐng)域模型中,針對各種不同的場景會使用特定的值對象類型。
此外,領(lǐng)域模型需要識別出聚合根。聚合根是一個通過組合其他實體而得到的實體。聚合根中的對象與外部沒有直接的關(guān)聯(lián),也就是不存在這樣的用例——不經(jīng)過根對象而直接使用這些對象。比如,Order 實體就是一個典型的聚合根。 Order 包含聚合的 OrderItem,而不包含 Product。 難以想象您使用一個OrderItem 而它并不來自 Order(即使這只是由specs決定的,譯者注:也就是通過規(guī)約查詢直接得到相應(yīng)的OderItem)。另一方面,您很可能具有這樣一些用例,您在其中使用不涉及訂單的 Product 實體。聚合根負(fù)責(zé)維護(hù)處于有效狀態(tài)的子對象并持久化這些對象。
最后,某些領(lǐng)域模型類(class)可以提供用于創(chuàng)建新實例的公共工廠方法,而不是構(gòu)造函數(shù)。如果模型類通常是獨立的并且實際上不是層次結(jié)構(gòu)的一部分,或者用于創(chuàng)建該類的步驟只是與客戶端相關(guān),則可以使用普通的構(gòu)造函數(shù)。但是,在使用聚合根這樣的復(fù)雜對象時,您還需要實例化之外的其他抽象級別。 DDD 引入了工廠對象(簡單一些的話,可以使用類中的工廠方法)方式,這種方式可將客戶端的需求與內(nèi)部的對象及其關(guān)系和規(guī)則分離開來。可以在 An Introduction to Domain Driven Design 中找到有關(guān) DDD 的清晰簡要的介紹。
讓我們重點分析一下 Customer 類。 根據(jù)上文所述,此處是可能的簽名:
Customer : Organization, IAggregateRoot { ... }
誰是您的客戶? 它是個人和/或組織? Party 模式建議您區(qū)別這兩者,并清晰地定義哪些屬性是公用的,哪些屬性僅屬于個人或組織?!按a1”中的代碼僅針對 Person 和 Organization。您可以根據(jù)業(yè)務(wù)領(lǐng)域的需要,將組織細(xì)分為非盈利組織和商業(yè)公司,從而細(xì)化代碼內(nèi)容。
代碼1 基于Party模式的類
Party { String Name { ; ; } PostalAddress MainPostalAddress { ; ; } } Person : Party { String Surname { ; ; } DateTime BirthDate { ; ; } String Ssn { ; ; } } Organization : Party { String VatId { ; ; } }
您必須始終記住,您的目標(biāo)是構(gòu)建一個可為您的實際業(yè)務(wù)領(lǐng)域精確建模的模型,而不是生成該業(yè)務(wù)的抽象表示。如果您的需求只涉及作為個體的客戶(Customer),那么 Party 模式不是必需的,即使該模式帶來了后續(xù)可擴展性。
聚合根是模型中的一個類,它表示一個獨立的實體——在與其他實體的關(guān)系中并不存在(one that doesn’t exist in relation to other entities,譯者注:也就是與其他實體不存在關(guān)聯(lián))。在大多數(shù)情況下,您的聚合根只是單獨的類,這些類不管理任何子對象,或者只是指向其他聚合的根。 “代碼2”顯示了更詳細(xì)的 Customer 類。
代碼2 作為聚合根的 Customer 類
Customer : Organization, IAggregateRoot { Customer CreateNewCustomer( String id, String companyName, String contactName) { ... } Customer() { } String Id { ; ; } ... IEnumerable<Order> Orders { { _Orders; } } Boolean IAggregateRoot.CanBeSaved { { IsValidForRegistration; } } Boolean IAggregateRoot.CanBeDeleted { { ; } } }
正如您所看到的,Customer 類實現(xiàn)了(自定義)IAggregateRoot 接口。 代碼如下:
IAggregateRoot { Boolean CanBeSaved { ; } Boolean CanBeDeleted { ; } }
成為聚合根意味著什么? 聚合根處理所包含的子聚合對象的持久化,并負(fù)責(zé)強制實施與該組對象相關(guān)的不變條件( invariant conditions)。因此,聚合根應(yīng)該能夠檢查整個聚合對象堆(stack)是否能被保存或刪除。獨立聚合根只返回 True,而不進(jìn)行任何進(jìn)一步檢查。
構(gòu)造函數(shù)是特定于類型的。如果對象只是一個類型(沒有聚合并且沒有復(fù)雜的初始化邏輯),那么使用普通的構(gòu)造函數(shù)會更好。工廠通常是一個有用的額外抽象層。工廠可以是實體類中的一個簡單的靜態(tài)方法,也可以是一個單獨的組件。使用工廠方法還可以讓代碼更具可讀性,因為通過它你可以清楚地知道為何要這樣實例化。如果使用構(gòu)造函數(shù),那么您在處理不同實例化場景時將受到更多的限制,因為構(gòu)造函數(shù)的方法名不能隨意更改(只能與類同名),只能通過簽名來識別它。特別是長簽名(有很多參數(shù)的構(gòu)造函數(shù)),在以后使用時會很難弄明白為什么要這樣實例化。 “代碼3”顯示了 Customer 類中的工廠方法。
代碼3 Customer 類中的工廠方法
Customer CreateNewCustomer( String id, String companyName, String contactName) { Contract.Requires<ArgumentNullException>( id != , ); Contract.Requires<ArgumentException>( !String.IsNullOrWhiteSpace(id), ); Contract.Requires<ArgumentNullException>( companyName != , ); Contract.Requires<ArgumentException>( !String.IsNullOrWhiteSpace(companyName), ); Contract.Requires<ArgumentNullException>( contactName != , ); Contract.Requires<ArgumentException>( !String.IsNullOrWhiteSpace(contactName), ); c = Customer { Id = id, Name = companyName, Orders = List<Order>(), ContactInfo = ContactInfo { ContactName = contactName } }; c; }
工廠方法是一個原子操作,可獲取輸入?yún)?shù)、執(zhí)行其作業(yè)并返回指定類型的新實例。應(yīng)確保返回的實例處于有效狀態(tài)。工廠負(fù)責(zé)履行所有已定義的內(nèi)部驗證規(guī)則。
工廠還需要驗證輸入數(shù)據(jù)。為此,可使用代碼協(xié)定(Code Contracts)前提條件來保證代碼的清晰和高可讀性。還可以使用后置條件來確保返回的實例處于有效狀態(tài),如下所示:
Contract.Ensures(Contract.Result<Customer>().IsValid());
如果在整個類中使用不變式(invariants),經(jīng)驗表明,您無法始終提供這些不變式。不變式的侵入性可能太強,特別是在復(fù)雜的大型模型中。代碼協(xié)定(Code Contracts)不變式有時可能過于嚴(yán)格地遵循規(guī)則集,而在您的代碼中,有時需要更多的靈活性。因此,最好對必須強制執(zhí)行不變式的區(qū)域進(jìn)行限制。
可能需要驗證領(lǐng)域類中的屬性,以確保必填字段不為空,文本沒有超出長度限制,并且相關(guān)數(shù)值處于規(guī)定的范圍內(nèi)等等。您還必須考慮進(jìn)行跨屬性驗證以及復(fù)雜的業(yè)務(wù)規(guī)則。如何進(jìn)行代碼驗證?
驗證涉及條件代碼,最終涉及組合某些 if 語句,并返回布爾值。不借助任何框架或技術(shù),純手工編寫驗證層也許可行,但實際上并不是一個好主意。這樣編寫出來的代碼的可讀性和后續(xù)改進(jìn)的方便性得不到保證,通過一些流暢的代碼工具庫(fluent libraries)可以改善這種情況。受實際業(yè)務(wù)規(guī)則的限制,驗證規(guī)則可能會經(jīng)常變化,您的實現(xiàn)必須考慮到這一點。因此,您不能只編寫針對當(dāng)前驗證規(guī)則的代碼,而是應(yīng)該編寫能夠適應(yīng)驗證規(guī)則變化的更靈活的代碼。
在驗證過程中,有時您希望傳入無效數(shù)據(jù)時給出提示,有時您只希望收集相關(guān)錯誤并將其報告給其他代碼層。記住,代碼協(xié)定不參與驗證過程,它只檢查各種條件,然后在條件不適用時引發(fā)異常。通過集中式錯誤處理程序,您可以從異常中進(jìn)行恢復(fù)并妥善降級。通常建議僅在領(lǐng)域?qū)嶓w中使用代碼協(xié)定,以便捕獲可能導(dǎo)致出現(xiàn)不一致狀態(tài)的潛在嚴(yán)重錯誤。也可以在工廠中使用代碼協(xié)定,在這種情況下,如果傳入的數(shù)據(jù)無效,代碼必須引發(fā)異常。是否在屬性的 setter 方法中使用代碼協(xié)定由您自己決定。我更喜歡采用更舒適的方式,通過特性類(Attribute)進(jìn)行驗證。但使用哪些Attribute呢?
Data Annotations 命名空間和企業(yè)庫 VAB 非常類似。這兩種框架均基于Attribute,可以使用表示自定義規(guī)則的自定義類對其進(jìn)行擴展。在這兩種情況下,您都可以定義跨屬性(property)驗證。最后,這兩種框架都提供了驗證API,用于評估實例并返回錯誤列表。這兩者有何區(qū)別?
Data Annotations 是 Microsoft .NET Framework 的一部分,不需要單獨下載。企業(yè)庫需要單獨下載,在大型項目中并不重要,但在企業(yè)應(yīng)用中可能需要批準(zhǔn),因此仍會產(chǎn)生問題。可以通過 NuGet 輕松安裝企業(yè)庫(請參閱本期專欄中的“使用 NuGet 管理項目庫”一文)。
企業(yè)庫 VAB 在以下方面優(yōu)于Data Annotations:可以通過 XML 規(guī)則集對其進(jìn)行配置。XML 規(guī)則集是您用于描述所需驗證的配置文件中的條目。不用說,您能夠以聲明方式更改某些內(nèi)容,甚至無需改動代碼。 “代碼4”顯示了一個示例規(guī)則集。
代碼4 企業(yè)庫規(guī)則集
<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" />
</&l