a亚洲精品_精品国产91乱码一区二区三区_亚洲精品在线免费观看视频_欧美日韩亚洲国产综合_久久久久久久久久久成人_在线区

首頁 > 編程 > C# > 正文

C# API中模型與它們的接口設計詳解

2019-10-29 19:59:04
字體:
來源:轉載
供稿:網友

關鍵要點

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

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

我想先定義一些術語,這些術語在其他文章中可能有更精確的定義,但對于我們來說這些已經足夠了。

數據模型(Data Model)

數據模型時包含數據(即屬性和集合)和行為的對象或對象圖。數據模型是本文的重點。

數據傳輸對象(Data Transfer Object,DTO)

DTO是只包含屬性和集合的對象或對象圖。一個真正的DTO沒有任何行為,而且幾乎是不可變的。

不過,在使用代碼生成工具生成DTO時,通常會使用一些簡單的接口(如INotifyPropertyChanged)。

對象圖(Object Graph)

一個對象圖由一個對象和所有可觸及的子對象組成。在討論數據模型和DTO時,我們所說的對象圖都是單向樹狀結構(循環圖是存在的,但它們會對序列化框架造成影響)。

領域模型(Domain Model)

領域模型是描述一組相關數據模型的更高級概念。

實體(Entity)

術語“實體”有許多定義,其中一些與“數據模型”基本相同。隨著nHibernate和Entity Framework的流行,這個術語一般是指與數據庫表一對一映射的DTO。

基于這個定義,實體可以用屬性來修飾,以便更精確地描述數據庫列和屬性之間的映射關系。它還支持從數據庫延遲加載子集合。

雖然可以通過擴展讓實體承擔數據模型的角色,但在應用業務邏輯之前,將實體映射到單獨的數據模型或DTO是更為常見的做法。

業務實體(Business Model)

不要與ORM的實體混淆了,這是數據模型的另一種呈現方式。

不可變對象(Immutable Object)

不可變對象不包含可以改變屬性的方法,它本身不是數據模型,但它可能出現在表示靜態查找數據的數據模型中。因為它們不能被修改,所以跨多個數據模型共享一個不可變對象是安全的。

數據訪問層(Data Access Layer,DAL)

在本文中,DAL包含了服務對象、存儲庫、直接數據庫調用、Web服務調用等。基本上包括了任何用于與外部依賴項(如數據存儲)發生交互的東西。

數據模型特征

真正的數據模型是可確定性測試(deterministically testable)的。也就是說,它們只由其他可確定性測試的數據類型組成。這意味著數據模型在運行時不能有任何外部依賴關系。

最后一點很重要。如果一個類在運行時與DAL耦合,那么它就不是數據模型。即使在編譯時使用IRepository接口來“解耦”類,也無法消除與外部依賴的關系。

在判斷什么是數據模型時,要小心那些“存活實體”。為了支持延遲加載,來自ORM的實體通常會包含一個對數據庫上下文的引用。這就又讓我們回到了非確定性行為的領域,實體行為的變化取決于上下文狀態以及對象的創建方式。

換句話說,數據模型的所有方法都應該是可預測的,而且這種預測只能基于它們的屬性值。

在父對象和子對象之間傳遞消息

父對象和子對象通常需要交互。如果做得不好,可能會導致難以理解的緊密交叉耦合。為了簡化問題,請遵循以下三條規則:

  1. 父對象可以直接與子對象的屬性和方法交互。
  2. 子對象只能通過觸發事件與父對象進行交互。
  3. 對象不能直接與兄弟對象交互,兄弟對象之間的消息必須通過共同的父對象來傳遞。

基于這樣的設計,可以將子對象分解出來,并在沒有父對象的情況下對其進行測試。測試本身可以監控只有父對象能夠處理的事件。

驗證——數據模型唯一必須具備的功能

接下來我想談談數據模型可能會實現的可選特性。但在開始之前,我想先討論每個數據模型必須具備的一個特性:驗證。

完全不處理數據的數據模型幾乎是不存在的。如果模型是來自文件、外部應用程序或用戶界面,就有可能會引入不一致或不合法的值。來自用戶界面的問題會更多,因為用戶通常需要逐個字段得填寫表單。

因為存在這些限制,所以不能在構造函數和屬性設置器中使用異常,就像你在其他類中使用異常一樣。不過可以驗證接口,為錯誤檢查提供一些靈活性。

.NET提供了一些開箱即用的驗證接口,不過每個人都有自己特定的需求。

IDataErrorInfo

IDataErrorInfo接口早就可以用了,不過現在基本被棄用,因為它用起來很麻煩。讓我們來看看它的屬性。

string Error {get;}:這個屬性有三個用途:

  • 報告對象級別的錯誤
  • 報告所有屬性級別的錯誤
  • 通過返回一個空字符串來表示不存在錯誤

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

正如你所看到的,Error屬性做的事情太多了,它將所有東西都拼湊成一個字符串,從而無法區分對象級別和屬性級別的驗證錯誤。如果你重新定義它,讓它只包含對象級錯誤,那么就無法知道對象作為整體是否包含錯誤。

至于索引器,你會怎么調用它?要訪問它的唯一方法是將該對象轉換成IDataErrorInfovariable。然后,很少有人會期望看到這樣的代碼:

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

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

INotifyDataErrorInfo的常規定義

我將分兩次討論INotifyDataErrorInfo接口。在本小節中,我將解釋本該如何使用INotifyDataErrorInfo,然后在下一個小節解釋我認為應該如何使用它。

INotifyDataErrorInfo接口旨在支持Silverlight 4中的異步驗證,其基本想法是修改屬性會觸發服務調用,被調用的服務最終會結束并更新錯誤狀態。

這個接口的唯一屬性是bool HasErrors {get;},不過關于如何實現這個屬性并沒有硬性規定。我們有兩個基本選項,但都不可行。

  1. 阻塞直到異步驗證完成,這樣會掛起UI。
  2. 立即返回,這會讓調用變得不確定,因為你不知道是否存在掛起的異步驗證請求。

如果只是進行一般的顯示,只要在發生EventHandler<DataErrorsChangedEventArgs> ErrorsChanged事件時更新HasErrors屬性即可。不過,如果你嘗試單擊“保存”按鈕同步檢查驗證狀態,那這就不是一個好辦法。

此外,ErrorsChanged理論上可以觸發兩次:一次是立即觸發,另一次是異步驗證完成后觸發。這可能會產生奇怪的UI效果,因為HasErrors會在兩種狀態之間切換。

最后是IEnumerable GetErrors(string propertyName)方法,這個方法用于驗證屬性。不過,你也可以傳給它一個null或空字符串來獲取對象級驗證錯誤。

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

不過缺乏類型安全并不是唯一的問題,這段話摘自它的文檔:

此方法返回一個IEnumerable,在異步驗證完成處理之前,可能會發生變化。綁定引擎因此能夠在添加、刪除或修改錯誤時自動更新用戶界面驗證反饋。

如果這個方法返回一個IObservable,或許就沒有問題。但是在這種情況下,IEnumerable能夠奏效的唯一方法是讓它在等待異步驗證完成之前阻塞。這樣仍然會導致UI掛起。

然后是封裝問題。如前所述,數據模型應該完全沒有任何外部依賴。屬性變化不應直接調用服務,因為這會使該類變得非常難以測試。如果你需要異步驗證某些內容,請在控制器或視圖模型中執行此操作。

INotifyDataErrorInfo的正確用法

盡管存在缺陷,但INotifyDataErrorInfo已經被用在很多UI框架中,所以我們無法忽略它。所幸的是,我們可以在不破壞兼容性的情況下重新定義它。

HasErrors屬性可以在其他屬性發生變化時進行同步更新。如果一個類實現了INotifyPropertyChanged,并且值發生變化,就會觸發PropertyChanged事件。

不管指定的屬性是有效還是無效,都應該觸發ErrorsChanged事件。如果對象級驗證已經發生變化,則應使用null或字符串觸發ErrorsChanged事件。

在新模型中,GetErrors應該始終返回一個支持IEnumerable<ValidationResult>的集合類。ValidationResult類提供了有用的信息,例如哪些屬性是驗證警告的一部分。這對于一些錯誤消息來說非常管用,比如“至少需要提供名字/姓氏中的一個”。

基于屬性的驗證

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

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

要創建自己的驗證屬性類,只需重寫IsValid方法。通常這用于單屬性驗證,不過也可以通過ValidationContext來訪問對象的其他屬性。

基于屬性的驗證的一個優點是,一些框架(比如ASP.NET MVC/WebAPI)已經選定它作為驗證接口。因為它是聲明式的,所以可以與UI共享驗證邏輯。

混合命令式和基于屬性的驗證

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

  • 驗證規則涉及多個屬性
  • 驗證規則涉及子對象
  • 驗證規則不會被其他類或屬性重用

命令式驗證的一個缺點是它只存在于服務器端,無法像使用基于屬性的驗證一樣自動與UI共享驗證邏輯。

命令式驗證的另一個限制是它需要使用共享接口,這樣才能讓應用程序的其余部分通過一致的方式觸發驗證。

空表單問題

當用戶在創建新記錄并未填寫所有必填字段時,就會出現空表單問題。在顯示表單時,你不希望看到每個字段都以紅色突出顯示。

為了解決這個問題,需要為模型提供兩個額外的方法:

  • 驗證:跨所有字段執行驗證,觸發類似“required”這樣的規則。
  • 清除錯誤:從對象中刪除所有已觸發的驗證錯誤。

對于這種模型,模型對象將從初始狀態開始。如果它在顯示給用戶之前已經包含了部分值,則應該在向用戶顯示之前調用清除錯誤的方法。

當用戶修改某個字段時,只驗證該字段。然后,在保存之前,可以調用驗證方法強制對模型進行全面檢查,包括非用戶修改的屬性。

理論上的驗證接口

我認為.NET的驗證接口應該看起來像這樣:

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庫中看到這個接口的實現。

IValidatableObject

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

我很喜歡這個方法,因為它可以觸發對象的完整驗證,所以它可以解決空表單問題。它返回ValidationResult對象,比原始字符串要好得多。

缺點是它接受ValidationContext對象作為參數,而幾乎沒有人知道如何使用這個類。以下是ValidationContext的屬性。

  • DisplayName:獲取或設置要驗證成員的名稱。
  • Items:獲取與此上下文關聯的鍵值對字典。
  • MemberName:獲取或設置要驗證成員的名稱。
  • ObjectInstance:獲取要驗證的對象。
  • ObjectType:獲取要驗證的對象類型。
  • ServiceContainer:獲取驗證服務容器。

關于如何使用這些屬性并沒有相關的指南。例如,什么時候應該設置MemberName屬性? DisplayName屬性實際上做了什么?字典中應該保存什么以及在驗證期間何時可以訪問它?

文檔中說它“可以通過任何實現IServiceProvider接口的服務添加自定義驗證”,但并沒有說明IServiceProvider.GetService(Type)方法需要支持哪些類型,因此無法利用此特性。

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

屬性變更通知

屬性變更通知在很多情況下都很有用,不過更常見的是與MVVM設計模式相關聯。屬性變更通知通過INotifyPropertyChanged接口公開出來,讓模型可以通知關聯的UI元素:基礎數據發生了變化。我們可以借此做一些有趣的事情,比如在后臺進程中更新模型或者在多個視圖之間共享模型。

實現屬性變更通知最簡單的辦法是每次在調用屬性設置器時觸發它們。雖然從技術方面看是可行的,但仍有一些性能方面的影響。

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

在上面的示例中,即使沒有不存在任何偵聽者,每個屬性變更通知讓然會分配一個新對象來保存屬性名稱。如果這些通知頻繁發生,則可能會觸發不必要的垃圾回收。為了避免這種情況,應該把PropertyChangedEventArgs對象緩存起來。

另一個問題是事件可能是不必要的。如果屬性值實際上沒有發生改變,就相當于無緣無故地觸發屏幕重繪。所以我們需要做一個簡單的檢查:

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); }} 

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

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

請注意,這種便利性可能會對性能造成一點影響。訪問內部字典比使用字段慢,并且值的裝箱操作可能會消除緩存PropertyChangedEventArgs所帶來的收益。

如果你只編寫服務器端代碼,可能會想“我沒有UI,所以我不需要這些”。如果真是這樣,或許你是對的。但有時候使用INotifyPropertyChanged可以簡化一些復雜的代碼。我建議服務器端開發人員至少將其視為一種選擇。

INotifyPropertyChanging

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

ISupportInitialize/ISupportInitializeNotification

ISupportInitialize的目的是臨時禁用屬性/集合變更通知、錯誤驗證等。要使用它,請在進行屬性變更之前先調用BeginInit。

當調用EndInit時,可以發送一個“everything changed”變更通知。這個是通過使用一個包含null或空屬性名稱的PropertyChangedEventArgs對象來完成的。

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

集合變更通知

正如我們需要知道單個屬性的變更一樣,我們也需要知道整個集合發生的變更。我們可以使用INotifyCollectionChanged接口來解決這個問題。

可惜的是,INotifyCollectionChanged遠不如它的名字所暗示的那么強大。從理論上講,CollectionChanged相關事件可以使用單個事件來告訴我們何時已將整組對象添加到集合中或從集合中刪除。但實際上,因為WPF中存在的設計缺陷導致無法實現這樣的功能。

INotifyCollectionChanged最著名的實現是ObservableCollection<T>。這個類旨在為每個添加或刪除的項目觸發一個單獨的CollectionChanged事件。在設計WPF時,它假設我們總是會使用ObservableCollection<T>,因此WPF不支持NotifyCollectionChangedEventArgs.NewItems具有多個項目的情況。

由于這個錯誤,沒有人可以實現帶有批量更新支持的INotifyCollectionChanged,除非他們100%確定集合類不會被用在WPF中。

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

類型安全的集合變更事件

除了沒有人使用的功能之外,INotifyCollectionChanged接口的另一個問題是,它不是類型安全的。如果類型對你來說非常重要,則必須執行(理論上)不安全的轉換或編寫代碼來處理永遠不會發生的情況。為了解決這個問題,我建議實現這個接口:

/// <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;}

這不僅解決了類型安全問題,而且不需要檢查NotifyCollectionChangedEventArgs.NewItems的大小。

集合中的屬性變更通知

.NET中另一個“缺失的接口”是能夠檢測集合中某個項目屬性何時發生變化。比方說,你有一個OrderCollection類,并且需要在屏幕上顯示TotalPrice屬性。為了保持這個屬性的準確性,你需要知道每個項目的單價何時發生變化。

對于我自己的集合,我經常會公開一個INotifyItemPropertyChanged接口,用于將集合中對象的任意PropertyChanged事件轉成單個ItemPropertyChanged事件。

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

變更跟蹤和撤消

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

變更跟蹤

從表面上看,IChangeTracking接口看起來好像很容易理解:對象發生變化或者沒有發生變化。但實際上它有點微妙。

從用戶界面角度來看,用戶通常想知道的是“這個對象或它的任何子對象是否發生變化了?”

從數據存儲角度來看,你希望知道對象本身是否發生了變化。

文檔里沒有提到這些,因為它沒有定義一個子對象是否被認為是“對象內容”的一部分。我個人偏好讓IsChanged包含子對象的變化,并為數據存儲添加單獨的IsChangedLocal屬性。

可恢復變更跟蹤

IRevertableChangeTracking添加了一個RejectChanges方法來撤消任何掛起的更改。這里存在同樣的問題,即這個方法適用于本地對象還是子對象。

我通常假設RejectChanges會遍歷對象圖,并拒絕所有掛起的變更。但在涉及集合屬性時,這可能有點蹊蹺,最好是將其封裝在類中,而不是嘗試構建臨時解決方案。

可編輯的對象

與IChangeTracking不同,IEditableObject專門用于UI場景中。具體地說,就是用在提供確定/取消語義的對話框和數據網格中。

在顯示對話框或將數據網格切換到編輯模式之前,必須調用BeginEdit來捕捉對象的快照。EndEdit清除快照,而CancelEdit將對象恢復到之前的狀態。請注意,大多數數據網格會自動為你調用這些方法。

如果你同時使用了IEditableObject和IRevertableChangeTracking,那么我建議將其實現為兩級撤消,并讓IEditableObject處于第二級。或者換句話說,在調用RejectChange時同時調用CancelEdit,但不能反過來。

遺失的屬性變更接口

在ORM集成中極有可能缺失一些接口。我們可以使用IChangeTracking來告訴ORM是否需要保存給定的記錄,但并沒有接口告訴我們哪些屬性已經發生改變。這意味著ORM需要單獨跟蹤發生變更的字段,或者假設所有內容都發生變化,并將整個對象重新保存到數據庫。

Equals、GetHashCode和IEquatable

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

問題是你不能使用可變屬性來安全地實現GetHashCode和Equals。字典會假設散列碼永遠不會改變,所以如果一個對象被當作字典的鍵,就會破壞字典的功能。

此外,對于數據模型來說,Equality究竟意味著什么?它們代表數據庫表中的同一行(即主鍵)?或者兩個對象的每個屬性都相同?不管你如何回答這個問題,你的團隊中的其他人必定會有不同的答案。

如果你覺得必須要有非默認的Equals或GetHashCode實現,請考慮創建一個IEqualityComparer<T>。它不屬于數據模型,所以其他人可以理解你的做法是非標準的行為。

同樣,你可能希望為排序提供一個或多個Comparer<T>類。

ICloneable

眾所周知,我們不應該實現ICloneable接口,因為我們從來都不知道一個對象克隆是深拷貝還是淺拷貝。

當然,這并不意味著你絕對不應該提供克隆方法。如果你選擇提供克隆方法,就應該非常清楚地了解被克隆的內容。或者可以將其稱為ShallowClone或DeepClone。

總結性思考

模型是構建和理解應用程序的基礎。你花在彌補缺口上的時間,比如不一致的命名約定、缺少的特性和不正確實現的接口,最終都會獲得回報。

關于作者

Jonathan Allen 在90年代后期開始為一家健康診所開發MIS項目,將逐步從Access和Excel遷移成為一個企業解決方案。在為金融行業開發自動交易系統五年后,他成為各種項目的顧問,其中包括機器人倉庫的用戶界面、癌癥研究軟件的中間層以及大型房地產保險公司的大數據解決方案。在空閑時間,他喜歡學習有關16世紀武術的東西。

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

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對VEVB武林網的支持。


注:相關教程知識閱讀請移步到c#教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 久久久久久久性 | 欧美日韩在线看 | 欧美成人激情视频 | 国产一区二区三区久久久久久久久 | 伊人网视频 | 韩国电影久久影院 | 国产精品久久久久久久久动漫 | 一本一道久久a久久精品综合蜜臀 | a级黄色毛片免费观看 | 久久久久无码国产精品一区 | 青青草视频播放 | 久久久久久久久久久蜜桃 | 久久久99国产精品免费 | 国产精品一区久久久久 | 久色视频在线 | 麻豆毛片 | 国产精品视频一区二区噜噜 | 特级黄色毛片 | 无码日韩精品一区二区免费 | 亚洲精品久久久 | 国产精品毛片一区二区在线看 | 欧美亚洲免费 | 黄色片av | 九色 在线| 在线日韩一区 | 美女一区 | 日韩精品久久久久 | 在线免费中文字幕 | 免费在线观看一区二区 | 国产精品天天干 | 欧美天堂在线 | 高清成人在线 | 成人超碰在线 | 久久久久久一区 | 日韩不卡 | 中文字幕高清在线 | 91小视频网站 | 福利视频一区二区三区 | 免费黄色在线网址 | 国产视频中文字幕 | 欧美日韩精品综合 |