C#API中模型与它们的接口设计详解-创新互联

关键要点

公司主营业务:网站制作、网站建设、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。创新互联是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。创新互联推出新疆免费做网站回馈大家。

可变模型应该具备自我验证的能力,并实现验证接口。
在共享对象时(特别是在跨线程共享时),考虑使用不可变模型。
考虑支持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 ErrorsChanged事件时更新HasErrors属性即可。不过,如果你尝试单击“保存”按钮同步检查验证状态,那这就不是一个好办法。

此外,ErrorsChanged理论上可以触发两次:一次是立即触发,另一次是异步验证完成后触发。这可能会产生奇怪的UI效果,因为HasErrors会在两种状态之间切换。

最后是IEnumerable GetErrors(string propertyName)方法,这个方法用于验证属性。不过,你也可以传给它一个null或空字符串来获取对象级验证错误。

它返回的是IEnumerable而不是IEnumerable,这让它看起来就像是一个C# 1的接口,而不是泛型。

不过缺乏类型安全并不是唯一的问题,这段话摘自它的文档:

此方法返回一个IEnumerable,在异步验证完成处理之前,可能会发生变化。绑定引擎因此能够在添加、删除或修改错误时自动更新用户界面验证反馈。

如果这个方法返回一个IObservable,或许就没有问题。但是在这种情况下,IEnumerable能够奏效的唯一方法是让它在等待异步验证完成之前阻塞。这样仍然会导致UI挂起。

然后是封装问题。如前所述,数据模型应该完全没有任何外部依赖。属性变化不应直接调用服务,因为这会使该类变得非常难以测试。如果你需要异步验证某些内容,请在控制器或视图模型中执行此操作。

INotifyDataErrorInfo的正确用法

尽管存在缺陷,但INotifyDataErrorInfo已经被用在很多UI框架中,所以我们无法忽略它。所幸的是,我们可以在不破坏兼容性的情况下重新定义它。

HasErrors属性可以在其他属性发生变化时进行同步更新。如果一个类实现了INotifyPropertyChanged,并且值发生变化,就会触发PropertyChanged事件。

不管指定的属性是有效还是无效,都应该触发ErrorsChanged事件。如果对象级验证已经发生变化,则应使用null或字符串触发ErrorsChanged事件。

在新模型中,GetErrors应该始终返回一个支持IEnumerable的集合类。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 GetErrors();

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

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

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

另外有需要云服务器可以了解下创新互联scvps.cn,海内外云服务器15元起步,三天无理由+7*72小时售后在线,公司持有idc许可证,提供“云服务器、裸金属服务器、高防服务器、香港服务器、美国服务器、虚拟主机、免备案服务器”等云主机租用服务以及企业上云的综合解决方案,具有“安全稳定、简单易用、服务可用性高、性价比高”等特点与优势,专为企业上云打造定制,能够满足用户丰富、多元化的应用场景需求。


网站标题:C#API中模型与它们的接口设计详解-创新互联
文章URL:http://scyanting.com/article/dijehh.html