时间:2019-11-4来源:本站原创作者:佚名

关键要点

可变模型应该具备自我验证的能力,并实现验证接口。

在共享对象时(特别是在跨线程共享时),考虑使用不可变模型。

考虑支持MVVM风格UI的单层和多层撤消。

在实现属性变更通知时避免不必要的内存分配。

不要覆盖模型的Equals和GetHashCode方法。

在传统的MVC、MVP、MVVM、WebMVC这些UI模式中,模型是一个公共元素。虽然有很多文章讨论这些架构中的视图和控制器,但几乎无一涉及模型。在本文中,我们将讨论模型本身以及相应的.NET接口。

我想先定义一些术语,这些术语在其他文章中可能有更精确的定义,但对于我们来说这些已经足够了。

数据模型(DataModel)

数据模型时包含数据(即属性和集合)和行为的对象或对象图。数据模型是本文的重点。

数据传输对象(DataTransferObject,DTO)

DTO是只包含属性和集合的对象或对象图。一个真正的DTO没有任何行为,而且几乎是不可变的。

不过,在使用代码生成工具生成DTO时,通常会使用一些简单的接口(如INotifyPropertyChanged)。

对象图(ObjectGraph)

一个对象图由一个对象和所有可触及的子对象组成。在讨论数据模型和DTO时,我们所说的对象图都是单向树状结构(循环图是存在的,但它们会对序列化框架造成影响)。

领域模型(DomainModel)

领域模型是描述一组相关数据模型的更高级概念。

实体(Entity)

术语“实体”有许多定义,其中一些与“数据模型”基本相同。随着nHibernate和EntityFramework的流行,这个术语一般是指与数据库表一对一映射的DTO。

基于这个定义,实体可以用属性来修饰,以便更精确地描述数据库列和属性之间的映射关系。它还支持从数据库延迟加载子集合。

虽然可以通过扩展让实体承担数据模型的角色,但在应用业务逻辑之前,将实体映射到单独的数据模型或DTO是更为常见的做法。

业务实体(BusinessModel)

不要与ORM的实体混淆了,这是数据模型的另一种呈现方式。

不可变对象(ImmutableObject)

不可变对象不包含可以改变属性的方法,它本身不是数据模型,但它可能出现在表示静态查找数据的数据模型中。因为它们不能被修改,所以跨多个数据模型共享一个不可变对象是安全的。

数据访问层(DataAccessLayer,DAL)

在本文中,DAL包含了服务对象、存储库、直接数据库调用、Web服务调用等。基本上包括了任何用于与外部依赖项(如数据存储)发生交互的东西。

真正的数据模型是可确定性测试(deterministicallytestable)的。也就是说,它们只由其他可确定性测试的数据类型组成。这意味着数据模型在运行时不能有任何外部依赖关系。

最后一点很重要。如果一个类在运行时与DAL耦合,那么它就不是数据模型。即使在编译时使用IRepository接口来“解耦”类,也无法消除与外部依赖的关系。

在判断什么是数据模型时,要小心那些“存活实体”。为了支持延迟加载,来自ORM的实体通常会包含一个对数据库上下文的引用。这就又让我们回到了非确定性行为的领域,实体行为的变化取决于上下文状态以及对象的创建方式。

换句话说,数据模型的所有方法都应该是可预测的,而且这种预测只能基于它们的属性值。

父对象和子对象通常需要交互。如果做得不好,可能会导致难以理解的紧密交叉耦合。为了简化问题,请遵循以下三条规则:

父对象可以直接与子对象的属性和方法交互。

子对象只能通过触发事件与父对象进行交互。

对象不能直接与兄弟对象交互,兄弟对象之间的消息必须通过共同的父对象来传递。

基于这样的设计,可以将子对象分解出来,并在没有父对象的情况下对其进行测试。测试本身可以监控只有父对象能够处理的事件。

接下来我想谈谈数据模型可能会实现的可选特性。但在开始之前,我想先讨论每个数据模型必须具备的一个特性:验证。

完全不处理数据的数据模型几乎是不存在的。如果模型是来自文件、外部应用程序或用户界面,就有可能会引入不一致或不合法的值。来自用户界面的问题会更多,因为用户通常需要逐个字段得填写表单。

因为存在这些限制,所以不能在构造函数和属性设置器中使用异常,就像你在其他类中使用异常一样。不过可以验证接口,为错误检查提供一些灵活性。

.NET提供了一些开箱即用的验证接口,不过每个人都有自己特定的需求。

IDataErrorInfo

IDataErrorInfo接口早就可以用了,不过现在基本被弃用,因为它用起来很麻烦。让我们来看看它的属性。

stringError{get;}:这个属性有三个用途:

报告对象级别的错误

报告所有属性级别的错误

通过返回一个空字符串来表示不存在错误

stringthis[stringcolumnName]{get;}:这个索引器属性将返回属性特定的错误。

正如你所看到的,Error属性做的事情太多了,它将所有东西都拼凑成一个字符串,从而无法区分对象级别和属性级别的验证错误。如果你重新定义它,让它只包含对象级错误,那么就无法知道对象作为整体是否包含错误。

至于索引器,你会怎么调用它?要访问它的唯一方法是将该对象转换成IDataErrorInfovariable。然后,很少有人会期望看到这样的代码:

varnameError=((IDataErrorInfo)customer)["Name"];

如果你的UI框架需要这个接口,我建议你将它放到一个基类中,并提供更合理的验证API。一旦加入真实的验证逻辑,甚至可以忽略IDataErrorInfo的存在。

INotifyDataErrorInfo的常规定义

我将分两次讨论INotifyDataErrorInfo接口。在本小节中,我将解释本该如何使用INotifyDataErrorInfo,然后在下一个小节解释我认为应该如何使用它。

INotifyDataErrorInfo接口旨在支持Silverlight4中的异步验证,其基本想法是修改属性会触发服务调用,被调用的服务最终会结束并更新错误状态。

这个接口的唯一属性是boolHasErrors{get;},不过关于如何实现这个属性并没有硬性规定。我们有两个基本选项,但都不可行。

阻塞直到异步验证完成,这样会挂起UI。

立即返回,这会让调用变得不确定,因为你不知道是否存在挂起的异步验证请求。

如果只是进行一般的显示,只要在发生EventHandlerErrorsChanged事件时更新HasErrors属性即可。不过,如果你尝试单击“保存”按钮同步检查验证状态,那这就不是一个好办法。

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

最后是IEnumerableGetErrors(stringpropertyName)方法,这个方法用于验证属性。不过,你也可以传给它一个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.NETMVC/WebAPI)已经选定它作为验证接口。因为它是声明式的,所以可以与UI共享验证逻辑。

混合命令式和基于属性的验证

虽然理论上可以使用验证属性来完成所有工作,但有时候使用普通代码可以更容易地实现严格的验证。这样做的原因如下:

验证规则涉及多个属性

验证规则涉及子对象

验证规则不会被其他类或属性重用

命令式验证的一个缺点是它只存在于服务器端,无法像使用基于属性的验证一样自动与UI共享验证逻辑。

命令式验证的另一个限制是它需要使用共享接口,这样才能让应用程序的其余部分通过一致的方式触发验证。

空表单问题

当用户在创建新记录并未填写所有必填字段时,就会出现空表单问题。在显示表单时,你不希望看到每个字段都以红色突出显示。

为了解决这个问题,需要为模型提供两个额外的方法:

验证:跨所有字段执行验证,触发类似“required”这样的规则。

清除错误:从对象中删除所有已触发的验证错误。

对于这种模型,模型对象将从初始状态开始。如果它在显示给用户之前已经包含了部分值,则应该在向用户显示之前调用清除错误的方法。

当用户修改某个字段时,只验证该字段。然后,在保存之前,可以调用验证方法强制对模型进行全面检查,包括非用户修改的属性。

理论上的验证接口

我认为.NET的验证接口应该看起来像这样:

publicinterfaceIValidatable

{

///Thisforcestheobjecttobe







































治疗白癜风去哪里最好
白癜风能否治愈

转载请注明原文网址:http://www.helimiaopu.com/bbqb/7538.html

------分隔线----------------------------