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