溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

C# API中模型與它們的接口設(shè)計(jì)詳解

發(fā)布時(shí)間:2020-09-13 21:40:07 來(lái)源:腳本之家 閱讀:254 作者:無(wú)明 欄目:編程語(yǔ)言

關(guān)鍵要點(diǎn)

可變模型應(yīng)該具備自我驗(yàn)證的能力,并實(shí)現(xiàn)驗(yàn)證接口。
在共享對(duì)象時(shí)(特別是在跨線(xiàn)程共享時(shí)),考慮使用不可變模型。
考慮支持MVVM風(fēng)格UI的單層和多層撤消。
在實(shí)現(xiàn)屬性變更通知時(shí)避免不必要的內(nèi)存分配。
不要覆蓋模型的Equals和GetHashCode方法。

在傳統(tǒng)的MVC、MVP、MVVM、Web MVC這些UI模式中,模型是一個(gè)公共元素。雖然有很多文章討論這些架構(gòu)中的視圖和控制器,但幾乎無(wú)一涉及模型。在本文中,我們將討論模型本身以及相應(yīng)的.NET接口。

我想先定義一些術(shù)語(yǔ),這些術(shù)語(yǔ)在其他文章中可能有更精確的定義,但對(duì)于我們來(lái)說(shuō)這些已經(jīng)足夠了。

數(shù)據(jù)模型(Data Model)

數(shù)據(jù)模型時(shí)包含數(shù)據(jù)(即屬性和集合)和行為的對(duì)象或?qū)ο髨D。數(shù)據(jù)模型是本文的重點(diǎn)。

數(shù)據(jù)傳輸對(duì)象(Data Transfer Object,DTO)

DTO是只包含屬性和集合的對(duì)象或?qū)ο髨D。一個(gè)真正的DTO沒(méi)有任何行為,而且?guī)缀跏遣豢勺兊摹?/p>

不過(guò),在使用代碼生成工具生成DTO時(shí),通常會(huì)使用一些簡(jiǎn)單的接口(如INotifyPropertyChanged)。

對(duì)象圖(Object Graph)

一個(gè)對(duì)象圖由一個(gè)對(duì)象和所有可觸及的子對(duì)象組成。在討論數(shù)據(jù)模型和DTO時(shí),我們所說(shuō)的對(duì)象圖都是單向樹(shù)狀結(jié)構(gòu)(循環(huán)圖是存在的,但它們會(huì)對(duì)序列化框架造成影響)。

領(lǐng)域模型(Domain Model)

領(lǐng)域模型是描述一組相關(guān)數(shù)據(jù)模型的更高級(jí)概念。

實(shí)體(Entity)

術(shù)語(yǔ)“實(shí)體”有許多定義,其中一些與“數(shù)據(jù)模型”基本相同。隨著nHibernate和Entity Framework的流行,這個(gè)術(shù)語(yǔ)一般是指與數(shù)據(jù)庫(kù)表一對(duì)一映射的DTO。

基于這個(gè)定義,實(shí)體可以用屬性來(lái)修飾,以便更精確地描述數(shù)據(jù)庫(kù)列和屬性之間的映射關(guān)系。它還支持從數(shù)據(jù)庫(kù)延遲加載子集合。

雖然可以通過(guò)擴(kuò)展讓實(shí)體承擔(dān)數(shù)據(jù)模型的角色,但在應(yīng)用業(yè)務(wù)邏輯之前,將實(shí)體映射到單獨(dú)的數(shù)據(jù)模型或DTO是更為常見(jiàn)的做法。

業(yè)務(wù)實(shí)體(Business Model)

不要與ORM的實(shí)體混淆了,這是數(shù)據(jù)模型的另一種呈現(xiàn)方式。

不可變對(duì)象(Immutable Object)

不可變對(duì)象不包含可以改變屬性的方法,它本身不是數(shù)據(jù)模型,但它可能出現(xiàn)在表示靜態(tài)查找數(shù)據(jù)的數(shù)據(jù)模型中。因?yàn)樗鼈儾荒鼙恍薷模钥缍鄠€(gè)數(shù)據(jù)模型共享一個(gè)不可變對(duì)象是安全的。

數(shù)據(jù)訪(fǎng)問(wèn)層(Data Access Layer,DAL)

在本文中,DAL包含了服務(wù)對(duì)象、存儲(chǔ)庫(kù)、直接數(shù)據(jù)庫(kù)調(diào)用、Web服務(wù)調(diào)用等?;旧习巳魏斡糜谂c外部依賴(lài)項(xiàng)(如數(shù)據(jù)存儲(chǔ))發(fā)生交互的東西。

數(shù)據(jù)模型特征

真正的數(shù)據(jù)模型是可確定性測(cè)試(deterministically testable)的。也就是說(shuō),它們只由其他可確定性測(cè)試的數(shù)據(jù)類(lèi)型組成。這意味著數(shù)據(jù)模型在運(yùn)行時(shí)不能有任何外部依賴(lài)關(guān)系。

最后一點(diǎn)很重要。如果一個(gè)類(lèi)在運(yùn)行時(shí)與DAL耦合,那么它就不是數(shù)據(jù)模型。即使在編譯時(shí)使用IRepository接口來(lái)“解耦”類(lèi),也無(wú)法消除與外部依賴(lài)的關(guān)系。

在判斷什么是數(shù)據(jù)模型時(shí),要小心那些“存活實(shí)體”。為了支持延遲加載,來(lái)自O(shè)RM的實(shí)體通常會(huì)包含一個(gè)對(duì)數(shù)據(jù)庫(kù)上下文的引用。這就又讓我們回到了非確定性行為的領(lǐng)域,實(shí)體行為的變化取決于上下文狀態(tài)以及對(duì)象的創(chuàng)建方式。

換句話(huà)說(shuō),數(shù)據(jù)模型的所有方法都應(yīng)該是可預(yù)測(cè)的,而且這種預(yù)測(cè)只能基于它們的屬性值。

在父對(duì)象和子對(duì)象之間傳遞消息

父對(duì)象和子對(duì)象通常需要交互。如果做得不好,可能會(huì)導(dǎo)致難以理解的緊密交叉耦合。為了簡(jiǎn)化問(wèn)題,請(qǐng)遵循以下三條規(guī)則:

  1. 父對(duì)象可以直接與子對(duì)象的屬性和方法交互。
  2. 子對(duì)象只能通過(guò)觸發(fā)事件與父對(duì)象進(jìn)行交互。
  3. 對(duì)象不能直接與兄弟對(duì)象交互,兄弟對(duì)象之間的消息必須通過(guò)共同的父對(duì)象來(lái)傳遞。

基于這樣的設(shè)計(jì),可以將子對(duì)象分解出來(lái),并在沒(méi)有父對(duì)象的情況下對(duì)其進(jìn)行測(cè)試。測(cè)試本身可以監(jiān)控只有父對(duì)象能夠處理的事件。

驗(yàn)證——數(shù)據(jù)模型唯一必須具備的功能

接下來(lái)我想談?wù)剶?shù)據(jù)模型可能會(huì)實(shí)現(xiàn)的可選特性。但在開(kāi)始之前,我想先討論每個(gè)數(shù)據(jù)模型必須具備的一個(gè)特性:驗(yàn)證。

完全不處理數(shù)據(jù)的數(shù)據(jù)模型幾乎是不存在的。如果模型是來(lái)自文件、外部應(yīng)用程序或用戶(hù)界面,就有可能會(huì)引入不一致或不合法的值。來(lái)自用戶(hù)界面的問(wèn)題會(huì)更多,因?yàn)橛脩?hù)通常需要逐個(gè)字段得填寫(xiě)表單。

因?yàn)榇嬖谶@些限制,所以不能在構(gòu)造函數(shù)和屬性設(shè)置器中使用異常,就像你在其他類(lèi)中使用異常一樣。不過(guò)可以驗(yàn)證接口,為錯(cuò)誤檢查提供一些靈活性。

.NET提供了一些開(kāi)箱即用的驗(yàn)證接口,不過(guò)每個(gè)人都有自己特定的需求。

IDataErrorInfo

IDataErrorInfo接口早就可以用了,不過(guò)現(xiàn)在基本被棄用,因?yàn)樗闷饋?lái)很麻煩。讓我們來(lái)看看它的屬性。

string Error {get;}:這個(gè)屬性有三個(gè)用途:

  • 報(bào)告對(duì)象級(jí)別的錯(cuò)誤
  • 報(bào)告所有屬性級(jí)別的錯(cuò)誤
  • 通過(guò)返回一個(gè)空字符串來(lái)表示不存在錯(cuò)誤

string this[string columnName] {get;}:這個(gè)索引器屬性將返回屬性特定的錯(cuò)誤。

正如你所看到的,Error屬性做的事情太多了,它將所有東西都拼湊成一個(gè)字符串,從而無(wú)法區(qū)分對(duì)象級(jí)別和屬性級(jí)別的驗(yàn)證錯(cuò)誤。如果你重新定義它,讓它只包含對(duì)象級(jí)錯(cuò)誤,那么就無(wú)法知道對(duì)象作為整體是否包含錯(cuò)誤。

至于索引器,你會(huì)怎么調(diào)用它?要訪(fǎng)問(wèn)它的唯一方法是將該對(duì)象轉(zhuǎn)換成IDataErrorInfovariable。然后,很少有人會(huì)期望看到這樣的代碼:

var nameError = ((IDataErrorInfo)customer)["Name"];

如果你的UI框架需要這個(gè)接口,我建議你將它放到一個(gè)基類(lèi)中,并提供更合理的驗(yàn)證API。一旦加入真實(shí)的驗(yàn)證邏輯,甚至可以忽略IDataErrorInfo的存在。

INotifyDataErrorInfo的常規(guī)定義

我將分兩次討論INotifyDataErrorInfo接口。在本小節(jié)中,我將解釋本該如何使用INotifyDataErrorInfo,然后在下一個(gè)小節(jié)解釋我認(rèn)為應(yīng)該如何使用它。

INotifyDataErrorInfo接口旨在支持Silverlight 4中的異步驗(yàn)證,其基本想法是修改屬性會(huì)觸發(fā)服務(wù)調(diào)用,被調(diào)用的服務(wù)最終會(huì)結(jié)束并更新錯(cuò)誤狀態(tài)。

這個(gè)接口的唯一屬性是bool HasErrors {get;},不過(guò)關(guān)于如何實(shí)現(xiàn)這個(gè)屬性并沒(méi)有硬性規(guī)定。我們有兩個(gè)基本選項(xiàng),但都不可行。

  1. 阻塞直到異步驗(yàn)證完成,這樣會(huì)掛起UI。
  2. 立即返回,這會(huì)讓調(diào)用變得不確定,因?yàn)槟悴恢朗欠翊嬖趻炱鸬漠惒津?yàn)證請(qǐng)求。

如果只是進(jìn)行一般的顯示,只要在發(fā)生EventHandler<DataErrorsChangedEventArgs> ErrorsChanged事件時(shí)更新HasErrors屬性即可。不過(guò),如果你嘗試單擊“保存”按鈕同步檢查驗(yàn)證狀態(tài),那這就不是一個(gè)好辦法。

此外,ErrorsChanged理論上可以觸發(fā)兩次:一次是立即觸發(fā),另一次是異步驗(yàn)證完成后觸發(fā)。這可能會(huì)產(chǎn)生奇怪的UI效果,因?yàn)镠asErrors會(huì)在兩種狀態(tài)之間切換。

最后是IEnumerable GetErrors(string propertyName)方法,這個(gè)方法用于驗(yàn)證屬性。不過(guò),你也可以傳給它一個(gè)null或空字符串來(lái)獲取對(duì)象級(jí)驗(yàn)證錯(cuò)誤。

它返回的是IEnumerable而不是IEnumerable<ValidationResult>,這讓它看起來(lái)就像是一個(gè)C# 1的接口,而不是泛型。

不過(guò)缺乏類(lèi)型安全并不是唯一的問(wèn)題,這段話(huà)摘自它的文檔:

此方法返回一個(gè)IEnumerable,在異步驗(yàn)證完成處理之前,可能會(huì)發(fā)生變化。綁定引擎因此能夠在添加、刪除或修改錯(cuò)誤時(shí)自動(dòng)更新用戶(hù)界面驗(yàn)證反饋。

如果這個(gè)方法返回一個(gè)IObservable,或許就沒(méi)有問(wèn)題。但是在這種情況下,IEnumerable能夠奏效的唯一方法是讓它在等待異步驗(yàn)證完成之前阻塞。這樣仍然會(huì)導(dǎo)致UI掛起。

然后是封裝問(wèn)題。如前所述,數(shù)據(jù)模型應(yīng)該完全沒(méi)有任何外部依賴(lài)。屬性變化不應(yīng)直接調(diào)用服務(wù),因?yàn)檫@會(huì)使該類(lèi)變得非常難以測(cè)試。如果你需要異步驗(yàn)證某些內(nèi)容,請(qǐng)?jiān)诳刂破骰蛞晥D模型中執(zhí)行此操作。

INotifyDataErrorInfo的正確用法

盡管存在缺陷,但I(xiàn)NotifyDataErrorInfo已經(jīng)被用在很多UI框架中,所以我們無(wú)法忽略它。所幸的是,我們可以在不破壞兼容性的情況下重新定義它。

HasErrors屬性可以在其他屬性發(fā)生變化時(shí)進(jìn)行同步更新。如果一個(gè)類(lèi)實(shí)現(xiàn)了INotifyPropertyChanged,并且值發(fā)生變化,就會(huì)觸發(fā)PropertyChanged事件。

不管指定的屬性是有效還是無(wú)效,都應(yīng)該觸發(fā)ErrorsChanged事件。如果對(duì)象級(jí)驗(yàn)證已經(jīng)發(fā)生變化,則應(yīng)使用null或字符串觸發(fā)ErrorsChanged事件。

在新模型中,GetErrors應(yīng)該始終返回一個(gè)支持IEnumerable<ValidationResult>的集合類(lèi)。ValidationResult類(lèi)提供了有用的信息,例如哪些屬性是驗(yàn)證警告的一部分。這對(duì)于一些錯(cuò)誤消息來(lái)說(shuō)非常管用,比如“至少需要提供名字/姓氏中的一個(gè)”。

基于屬性的驗(yàn)證

我們可以使用基于屬性的驗(yàn)證完成很多工作,雖然這樣并不適合所有的情況。方法是在屬性上放置ValidationAttribute的子類(lèi)。這里有些例子:

  • CreditCardAttribute
  • EmailAddressAttribute
  • EnumDataTypeAttribute
  • FileExtensionsAttribute
  • PhoneAttribute
  • UrlAttribute
  • MaxLengthAttribute
  • MinLengthAttribute
  • RangeAttribute
  • RegularExpressionAttribute
  • RequiredAttribute
  • StringLengthAttribute

要?jiǎng)?chuàng)建自己的驗(yàn)證屬性類(lèi),只需重寫(xiě)IsValid方法。通常這用于單屬性驗(yàn)證,不過(guò)也可以通過(guò)ValidationContext來(lái)訪(fǎng)問(wèn)對(duì)象的其他屬性。

基于屬性的驗(yàn)證的一個(gè)優(yōu)點(diǎn)是,一些框架(比如ASP.NET MVC/WebAPI)已經(jīng)選定它作為驗(yàn)證接口。因?yàn)樗锹暶魇降?,所以可以與UI共享驗(yàn)證邏輯。

混合命令式和基于屬性的驗(yàn)證

雖然理論上可以使用驗(yàn)證屬性來(lái)完成所有工作,但有時(shí)候使用普通代碼可以更容易地實(shí)現(xiàn)嚴(yán)格的驗(yàn)證。這樣做的原因如下:

  • 驗(yàn)證規(guī)則涉及多個(gè)屬性
  • 驗(yàn)證規(guī)則涉及子對(duì)象
  • 驗(yàn)證規(guī)則不會(huì)被其他類(lèi)或?qū)傩灾赜?/li>

命令式驗(yàn)證的一個(gè)缺點(diǎn)是它只存在于服務(wù)器端,無(wú)法像使用基于屬性的驗(yàn)證一樣自動(dòng)與UI共享驗(yàn)證邏輯。

命令式驗(yàn)證的另一個(gè)限制是它需要使用共享接口,這樣才能讓?xiě)?yīng)用程序的其余部分通過(guò)一致的方式觸發(fā)驗(yàn)證。

空表單問(wèn)題

當(dāng)用戶(hù)在創(chuàng)建新記錄并未填寫(xiě)所有必填字段時(shí),就會(huì)出現(xiàn)空表單問(wèn)題。在顯示表單時(shí),你不希望看到每個(gè)字段都以紅色突出顯示。

為了解決這個(gè)問(wèn)題,需要為模型提供兩個(gè)額外的方法:

  • 驗(yàn)證:跨所有字段執(zhí)行驗(yàn)證,觸發(fā)類(lèi)似“required”這樣的規(guī)則。
  • 清除錯(cuò)誤:從對(duì)象中刪除所有已觸發(fā)的驗(yàn)證錯(cuò)誤。

對(duì)于這種模型,模型對(duì)象將從初始狀態(tài)開(kāi)始。如果它在顯示給用戶(hù)之前已經(jīng)包含了部分值,則應(yīng)該在向用戶(hù)顯示之前調(diào)用清除錯(cuò)誤的方法。

當(dāng)用戶(hù)修改某個(gè)字段時(shí),只驗(yàn)證該字段。然后,在保存之前,可以調(diào)用驗(yàn)證方法強(qiáng)制對(duì)模型進(jìn)行全面檢查,包括非用戶(hù)修改的屬性。

理論上的驗(yàn)證接口

我認(rèn)為.NET的驗(yàn)證接口應(yīng)該看起來(lái)像這樣:

public interface IValidatable
{
 /// This forces the object to be completely revalidated.
 bool Validate();

 /// Clears the error collections and the HasErrors property
 void ClearErrors();

 /// Returns True if there are any errors.
 bool HasErrors { get; }

 /// Returns a collection of object-level errors.
 ReadOnlyCollection<ValidationResult> GetErrors();

 /// Returns a collection of property-level errors.
 ReadOnlyCollection<ValidationResult> GetErrors(string propertyName);

 /// Returns a collection of all errors (object and property level).
 ReadOnlyCollection<ValidationResult> GetAllErrors();

 /// Raised when the errors collection has changed.
 event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
}

你可以在Tortuga Anchor庫(kù)中看到這個(gè)接口的實(shí)現(xiàn)。

IValidatableObject

如果不簡(jiǎn)要討論下IValidatableObject接口,那就是我的失職。這個(gè)接口只有一個(gè)方法IEnumerable<ValidationResult> Validate(ValidationContext validationContext)。

我很喜歡這個(gè)方法,因?yàn)樗梢杂|發(fā)對(duì)象的完整驗(yàn)證,所以它可以解決空表單問(wèn)題。它返回ValidationResult對(duì)象,比原始字符串要好得多。

缺點(diǎn)是它接受ValidationContext對(duì)象作為參數(shù),而幾乎沒(méi)有人知道如何使用這個(gè)類(lèi)。以下是ValidationContext的屬性。

  • DisplayName:獲取或設(shè)置要驗(yàn)證成員的名稱(chēng)。
  • Items:獲取與此上下文關(guān)聯(lián)的鍵值對(duì)字典。
  • MemberName:獲取或設(shè)置要驗(yàn)證成員的名稱(chēng)。
  • ObjectInstance:獲取要驗(yàn)證的對(duì)象。
  • ObjectType:獲取要驗(yàn)證的對(duì)象類(lèi)型。
  • ServiceContainer:獲取驗(yàn)證服務(wù)容器。

關(guān)于如何使用這些屬性并沒(méi)有相關(guān)的指南。例如,什么時(shí)候應(yīng)該設(shè)置MemberName屬性? DisplayName屬性實(shí)際上做了什么?字典中應(yīng)該保存什么以及在驗(yàn)證期間何時(shí)可以訪(fǎng)問(wèn)它?

文檔中說(shuō)它“可以通過(guò)任何實(shí)現(xiàn)IServiceProvider接口的服務(wù)添加自定義驗(yàn)證”,但并沒(méi)有說(shuō)明IServiceProvider.GetService(Type)方法需要支持哪些類(lèi)型,因此無(wú)法利用此特性。

總而言之,ValidationContext類(lèi)想要做所有的事情,但由于糟糕的API設(shè)計(jì)和幾乎沒(méi)有詳盡的文檔,它變得一無(wú)是處。由于沒(méi)有UI框架使用這個(gè)接口,所以沒(méi)有理由支持它或IValidatableObject接口。

屬性變更通知

屬性變更通知在很多情況下都很有用,不過(guò)更常見(jiàn)的是與MVVM設(shè)計(jì)模式相關(guān)聯(lián)。屬性變更通知通過(guò)INotifyPropertyChanged接口公開(kāi)出來(lái),讓模型可以通知關(guān)聯(lián)的UI元素:基礎(chǔ)數(shù)據(jù)發(fā)生了變化。我們可以借此做一些有趣的事情,比如在后臺(tái)進(jìn)程中更新模型或者在多個(gè)視圖之間共享模型。

實(shí)現(xiàn)屬性變更通知最簡(jiǎn)單的辦法是每次在調(diào)用屬性設(shè)置器時(shí)觸發(fā)它們。雖然從技術(shù)方面看是可行的,但仍有一些性能方面的影響。

public string Name
{
 get { return m_Name; }
 set
 {
 m_Name = value;
 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
 }
}

在上面的示例中,即使沒(méi)有不存在任何偵聽(tīng)者,每個(gè)屬性變更通知讓然會(huì)分配一個(gè)新對(duì)象來(lái)保存屬性名稱(chēng)。如果這些通知頻繁發(fā)生,則可能會(huì)觸發(fā)不必要的垃圾回收。為了避免這種情況,應(yīng)該把PropertyChangedEventArgs對(duì)象緩存起來(lái)。

另一個(gè)問(wèn)題是事件可能是不必要的。如果屬性值實(shí)際上沒(méi)有發(fā)生改變,就相當(dāng)于無(wú)緣無(wú)故地觸發(fā)屏幕重繪。所以我們需要做一個(gè)簡(jiǎn)單的檢查:

static readonly PropertyChangedEventArgs NameProperty = new PropertyChangedEventArgs(nameof(Name));
public string Name
{
 get { return m_Name; }
 set
 {
 if (m_Name == value)
  return;
 m_Name = value;
 PropertyChanged?.Invoke(this, NameProperty);
 }
} 

這個(gè)過(guò)程可能非常繁瑣,因此就有了“MVVM框架”,用來(lái)減少這些噪音。Get和Set方法與內(nèi)部字典一起使用,用來(lái)維護(hù)狀態(tài)。通過(guò)這種方式,可以為我們處理PropertyChangedEventArgs緩存和屬性值變更改檢查。具體細(xì)節(jié)會(huì)有所不同,但它們或多或少看起來(lái)像這個(gè)來(lái)自Tortuga Anchor的例子。

public string Name
{
 get => Get<string>();
 set => Set(value);
}

請(qǐng)注意,這種便利性可能會(huì)對(duì)性能造成一點(diǎn)影響。訪(fǎng)問(wèn)內(nèi)部字典比使用字段慢,并且值的裝箱操作可能會(huì)消除緩存PropertyChangedEventArgs所帶來(lái)的收益。

如果你只編寫(xiě)服務(wù)器端代碼,可能會(huì)想“我沒(méi)有UI,所以我不需要這些”。如果真是這樣,或許你是對(duì)的。但有時(shí)候使用INotifyPropertyChanged可以簡(jiǎn)化一些復(fù)雜的代碼。我建議服務(wù)器端開(kāi)發(fā)人員至少將其視為一種選擇。

INotifyPropertyChanging

這個(gè)是INotifyPropertyChanged的孿生兄弟,會(huì)在屬性值發(fā)生變更之前觸發(fā)。其目的是讓消費(fèi)者緩存先前的值。LINQ和Entity Framework等ORM框架可能會(huì)利用這些信息進(jìn)行跟蹤。

ISupportInitialize/ISupportInitializeNotification

ISupportInitialize的目的是臨時(shí)禁用屬性/集合變更通知、錯(cuò)誤驗(yàn)證等。要使用它,請(qǐng)?jiān)谶M(jìn)行屬性變更之前先調(diào)用BeginInit。

當(dāng)調(diào)用EndInit時(shí),可以發(fā)送一個(gè)“everything changed”變更通知。這個(gè)是通過(guò)使用一個(gè)包含null或空屬性名稱(chēng)的PropertyChangedEventArgs對(duì)象來(lái)完成的。

如果希望在初始化完成時(shí)收到通知,可以給ISupportInitializeNotification接口添加Initialized事件和IsInitialized屬性。

集合變更通知

正如我們需要知道單個(gè)屬性的變更一樣,我們也需要知道整個(gè)集合發(fā)生的變更。我們可以使用INotifyCollectionChanged接口來(lái)解決這個(gè)問(wèn)題。

可惜的是,INotifyCollectionChanged遠(yuǎn)不如它的名字所暗示的那么強(qiáng)大。從理論上講,CollectionChanged相關(guān)事件可以使用單個(gè)事件來(lái)告訴我們何時(shí)已將整組對(duì)象添加到集合中或從集合中刪除。但實(shí)際上,因?yàn)閃PF中存在的設(shè)計(jì)缺陷導(dǎo)致無(wú)法實(shí)現(xiàn)這樣的功能。

INotifyCollectionChanged最著名的實(shí)現(xiàn)是ObservableCollection<T>。這個(gè)類(lèi)旨在為每個(gè)添加或刪除的項(xiàng)目觸發(fā)一個(gè)單獨(dú)的CollectionChanged事件。在設(shè)計(jì)WPF時(shí),它假設(shè)我們總是會(huì)使用ObservableCollection<T>,因此WPF不支持NotifyCollectionChangedEventArgs.NewItems具有多個(gè)項(xiàng)目的情況。

由于這個(gè)錯(cuò)誤,沒(méi)有人可以實(shí)現(xiàn)帶有批量更新支持的INotifyCollectionChanged,除非他們100%確定集合類(lèi)不會(huì)被用在WPF中。

因此,我的建議是不要試圖從頭開(kāi)始創(chuàng)建自定義集合類(lèi)。只需使用ObservableCollection<T>或ReadOnlyObservableCollection<T>作為基類(lèi),然后在其上添加所需的任何附加特性。

類(lèi)型安全的集合變更事件

除了沒(méi)有人使用的功能之外,INotifyCollectionChanged接口的另一個(gè)問(wèn)題是,它不是類(lèi)型安全的。如果類(lèi)型對(duì)你來(lái)說(shuō)非常重要,則必須執(zhí)行(理論上)不安全的轉(zhuǎn)換或編寫(xiě)代碼來(lái)處理永遠(yuǎn)不會(huì)發(fā)生的情況。為了解決這個(gè)問(wèn)題,我建議實(shí)現(xiàn)這個(gè)接口:

/// <summary>
/// This is a type-safe version of INotifyCollectionChanged
/// </summary>
/// <typeparam name="T"></typeparam>
public interface INotifyCollectionChanged<T>
{
 /// <summary>
 /// This type safe event fires after an item is added to the collection no matter how it is added.
 /// </summary>
 /// <remarks>Triggered by InsertItem and SetItem</remarks>
 event EventHandler<ItemEventArgs<T>> ItemAdded;


 /// <summary>
 /// This type safe event fires after an item is removed from the collection no matter how it is removed.
 /// </summary>
 /// <remarks>Triggered by SetItem, RemoveItem, and ClearItems</remarks>
 event EventHandler<ItemEventArgs<T>> ItemRemoved;
}

這不僅解決了類(lèi)型安全問(wèn)題,而且不需要檢查NotifyCollectionChangedEventArgs.NewItems的大小。

集合中的屬性變更通知

.NET中另一個(gè)“缺失的接口”是能夠檢測(cè)集合中某個(gè)項(xiàng)目屬性何時(shí)發(fā)生變化。比方說(shuō),你有一個(gè)OrderCollection類(lèi),并且需要在屏幕上顯示TotalPrice屬性。為了保持這個(gè)屬性的準(zhǔn)確性,你需要知道每個(gè)項(xiàng)目的單價(jià)何時(shí)發(fā)生變化。

對(duì)于我自己的集合,我經(jīng)常會(huì)公開(kāi)一個(gè)INotifyItemPropertyChanged接口,用于將集合中對(duì)象的任意PropertyChanged事件轉(zhuǎn)成單個(gè)ItemPropertyChanged事件。

為此,集合需要在將對(duì)象添加到集合或從集合中移除時(shí)附加和移除事件處理程序。

變更跟蹤和撤消

雖然使用不是很頻繁,.NET還是提供了專(zhuān)門(mén)用于跟蹤對(duì)象變更的接口,這些接口甚至還提供了撤消功能。

變更跟蹤

從表面上看,IChangeTracking接口看起來(lái)好像很容易理解:對(duì)象發(fā)生變化或者沒(méi)有發(fā)生變化。但實(shí)際上它有點(diǎn)微妙。

從用戶(hù)界面角度來(lái)看,用戶(hù)通常想知道的是“這個(gè)對(duì)象或它的任何子對(duì)象是否發(fā)生變化了?”

從數(shù)據(jù)存儲(chǔ)角度來(lái)看,你希望知道對(duì)象本身是否發(fā)生了變化。

文檔里沒(méi)有提到這些,因?yàn)樗鼪](méi)有定義一個(gè)子對(duì)象是否被認(rèn)為是“對(duì)象內(nèi)容”的一部分。我個(gè)人偏好讓IsChanged包含子對(duì)象的變化,并為數(shù)據(jù)存儲(chǔ)添加單獨(dú)的IsChangedLocal屬性。

可恢復(fù)變更跟蹤

IRevertableChangeTracking添加了一個(gè)RejectChanges方法來(lái)撤消任何掛起的更改。這里存在同樣的問(wèn)題,即這個(gè)方法適用于本地對(duì)象還是子對(duì)象。

我通常假設(shè)RejectChanges會(huì)遍歷對(duì)象圖,并拒絕所有掛起的變更。但在涉及集合屬性時(shí),這可能有點(diǎn)蹊蹺,最好是將其封裝在類(lèi)中,而不是嘗試構(gòu)建臨時(shí)解決方案。

可編輯的對(duì)象

與IChangeTracking不同,IEditableObject專(zhuān)門(mén)用于UI場(chǎng)景中。具體地說(shuō),就是用在提供確定/取消語(yǔ)義的對(duì)話(huà)框和數(shù)據(jù)網(wǎng)格中。

在顯示對(duì)話(huà)框或?qū)?shù)據(jù)網(wǎng)格切換到編輯模式之前,必須調(diào)用BeginEdit來(lái)捕捉對(duì)象的快照。EndEdit清除快照,而CancelEdit將對(duì)象恢復(fù)到之前的狀態(tài)。請(qǐng)注意,大多數(shù)數(shù)據(jù)網(wǎng)格會(huì)自動(dòng)為你調(diào)用這些方法。

如果你同時(shí)使用了IEditableObject和IRevertableChangeTracking,那么我建議將其實(shí)現(xiàn)為兩級(jí)撤消,并讓IEditableObject處于第二級(jí)?;蛘邠Q句話(huà)說(shuō),在調(diào)用RejectChange時(shí)同時(shí)調(diào)用CancelEdit,但不能反過(guò)來(lái)。

遺失的屬性變更接口

在ORM集成中極有可能缺失一些接口。我們可以使用IChangeTracking來(lái)告訴ORM是否需要保存給定的記錄,但并沒(méi)有接口告訴我們哪些屬性已經(jīng)發(fā)生改變。這意味著ORM需要單獨(dú)跟蹤發(fā)生變更的字段,或者假設(shè)所有內(nèi)容都發(fā)生變化,并將整個(gè)對(duì)象重新保存到數(shù)據(jù)庫(kù)。

Equals、GetHashCode和IEquatable

這是我建議避免的一系列特性。根據(jù)我們的定義,數(shù)據(jù)模型是可變的。如果它們是不可變的,那么上述的接口都沒(méi)有任何意義。

問(wèn)題是你不能使用可變屬性來(lái)安全地實(shí)現(xiàn)GetHashCode和Equals。字典會(huì)假設(shè)散列碼永遠(yuǎn)不會(huì)改變,所以如果一個(gè)對(duì)象被當(dāng)作字典的鍵,就會(huì)破壞字典的功能。

此外,對(duì)于數(shù)據(jù)模型來(lái)說(shuō),Equality究竟意味著什么?它們代表數(shù)據(jù)庫(kù)表中的同一行(即主鍵)?或者兩個(gè)對(duì)象的每個(gè)屬性都相同?不管你如何回答這個(gè)問(wèn)題,你的團(tuán)隊(duì)中的其他人必定會(huì)有不同的答案。

如果你覺(jué)得必須要有非默認(rèn)的Equals或GetHashCode實(shí)現(xiàn),請(qǐng)考慮創(chuàng)建一個(gè)IEqualityComparer<T>。它不屬于數(shù)據(jù)模型,所以其他人可以理解你的做法是非標(biāo)準(zhǔn)的行為。

同樣,你可能希望為排序提供一個(gè)或多個(gè)Comparer<T>類(lèi)。

ICloneable

眾所周知,我們不應(yīng)該實(shí)現(xiàn)ICloneable接口,因?yàn)槲覀儚膩?lái)都不知道一個(gè)對(duì)象克隆是深拷貝還是淺拷貝。

當(dāng)然,這并不意味著你絕對(duì)不應(yīng)該提供克隆方法。如果你選擇提供克隆方法,就應(yīng)該非常清楚地了解被克隆的內(nèi)容。或者可以將其稱(chēng)為ShallowClone或DeepClone。

總結(jié)性思考

模型是構(gòu)建和理解應(yīng)用程序的基礎(chǔ)。你花在彌補(bǔ)缺口上的時(shí)間,比如不一致的命名約定、缺少的特性和不正確實(shí)現(xiàn)的接口,最終都會(huì)獲得回報(bào)。

關(guān)于作者

Jonathan Allen 在90年代后期開(kāi)始為一家健康診所開(kāi)發(fā)MIS項(xiàng)目,將逐步從Access和Excel遷移成為一個(gè)企業(yè)解決方案。在為金融行業(yè)開(kāi)發(fā)自動(dòng)交易系統(tǒng)五年后,他成為各種項(xiàng)目的顧問(wèn),其中包括機(jī)器人倉(cāng)庫(kù)的用戶(hù)界面、癌癥研究軟件的中間層以及大型房地產(chǎn)保險(xiǎn)公司的大數(shù)據(jù)解決方案。在空閑時(shí)間,他喜歡學(xué)習(xí)有關(guān)16世紀(jì)武術(shù)的東西。

查看英文原文:Models and Their Interfaces in C# API Design

總結(jié)

以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)億速云的支持。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI