采用一个自创的"验证框架"实现对数据实体的验证[设计篇]

简介:

没有想到自己头脑发热写了一个简陋版本的所谓“验证框架”能够得到众多网友的推荐。个人觉得这个验证框架有两个主要的特点是:提供CompositeValidator使复杂逻辑判断成为可能提供多验证规则的支持。《编程篇》中,我主要介绍了如何通过自定义特性的方式进行验证规则的定义,在本篇中我主要来介绍该验证框架的设计原理和实现。

一、核心三人组:Validator、ValidatorAttribute和ValidationError

应该说整个验证框架的核心体系只包含如下三中类型:Validator、ValidatorAttribute和ValidationError

image

  • Validator:所有的验证逻辑均实现在相应的“验证器”中,具体的验证器均直接或者间接继承自Validator这个抽象基类;
  • ValidatorAttribute:上述的验证器通过对应的自定义特性(Attribute)的方式应用到相应的数据实体类的属性上,ValidatorAttribute是这些特性的基类;
  • ValidationError:在Validator进行数据验证的时候,如果数据实体对象顺利通过验证,则返回Null,否则验证的错误信息封装成一个ValidationError对象返回。

上面的类图反映了上述三个核心类型的属性和操作,以及它们之间的关系。Validator通过Validate方法对传入的数据实体进行验证,验证失败的错误结果以ValidationError对象的形式返回;通过将相应的Validator应用到数据类型的目标属性上的ValidatorAttribute最终需要完成对Validator的创建

实际上,上述三个类型的定义都比较简单,我们先来看看Validator这个抽象类的定义。如下面的代码所示,Validator具有一个MessageTemplate的属性,表示验证错误信息的模板,该模板具有一些预定义的占位符。这些占位符可以包括与具体Validator无关的一般意义的对象,比如{PropertyName}、{PropertyValue}表示目标属性名和属性值,也包括一些具体Validator专有的占位符,比如编程篇》提到的GreaterThanValidator的{LowerBound}和LessThanValidator的{UpperBound}。虚FormatMessage方法用于对MessageTemplate进行格式化,即通过相应的值来替换对应的占位符。在这里将被验证的值替换掉{PropertyValue}占位符。最终的验证通过抽象方法Validate体现

   1: public abstract class Validator
   2: {
   3:     public string MessageTemplate { get; protected set; }
   4:     public abstract ValidationError Validate(object objectToValidate);
   5:     public Validator(string messageTemplate)
   6:     {
   7:         this.MessageTemplate = messageTemplate ?? string.Empty;
   8:     }
   9:     public virtual void FormatMessage(object objectToValidate)
  10:     {
  11:         this.MessageTemplate = this.MessageTemplate.Replace("{PropertyValue}", objectToValidate.ToString());
  12:     }
  13:     protected  ValidationError CreateValidationError(object objectToValidate)
  14:     {
  15:         Guard.ArgumentNotNull(objectToValidate, "objectToValidate");
  16:         return new ValidationError(this.MessageTemplate, objectToValidate, this);
  17:     }
  18: }

我们接着分析ValidatorAttribute的定义。如下面提供的代码片断所示,这是一个继承自Attribute的抽象类。MessageTemplate属性无需多说,RuleName属性表示验证规则的名称。而Tag是为了灵活实现对消息模板格式化的需要,你可以在MessageTemplate中定义{Tag}占位符,然后通过该属性指定替换它的值。ValidatorAttribute同样定义需方法FormatMessage,在这里我们用属性名称替换{PropertyName}占位符。我们的框架最终需要通过ValidatorAttribute创建相应的Validator,这个操作以抽象方法CreateValidator方法提供。

   1: [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
   2: public abstract class ValidatorAttribute: Attribute
   3: {
   4:     public string RuleName { get; set; }
   5:     public string Tag { get; set; }
   6:     public string MessageTemplate { get; private set; }
   7:     public abstract Validator CreateValidator();
   8:  
   9:     public ValidatorAttribute(string messageTemplate)
  10:     {
  11:         Guard.ArgumentNotNullOrEmpty(messageTemplate, "messageTemplate");
  12:         this.MessageTemplate = messageTemplate;
  13:         this.RuleName = string.Empty;
  14:     }
  15:     public virtual void FormatMessage(PropertyInfo property)
  16:     {
  17:         this.MessageTemplate = this.MessageTemplate.Replace("{PropertyName}", property.Name)
  18:             .Replace("{Tag}", this.Tag);
  19:     }
  20: }

表示验证失败信息的ValidationError,我尽量将其定义的简单一点。它包含Message、Target和Validtor三个属性,分别表示错误消息、验证的目标对象和采用的Validator。

   1: public class ValidationError
   2: {
   3:     public string       Message { get; internal set; }
   4:     public object       Target { get; internal set; }
   5:     public Validator    Validator { get; internal set; }
   6:  
   7:     public ValidationError(string message, object target, Validator validator)
   8:     {
   9:         Guard.ArgumentNotNull(message, "message");
  10:         Guard.ArgumentNotNull(target, "target");
  11:         Guard.ArgumentNotNull(validator, "validator");
  12:  
  13:         this.Message = message;
  14:         this.Target = target;
  15:         this.Validator = validator;
  16:     }
  17: }

二、一个特殊但却意义重大的Validator:CompositeValidator

正如开篇所说,这个框架一个重要的可取之处在于能够提供对复杂逻辑运算的支持,而这是通过CompositeValidator这种特殊的Validator来实现的。我们提要提供两种具体的CompositeValidator:AndCompositeValidatorOrCompositeValidator,分别用于进行“逻辑与”和“逻辑或”的逻辑判断。它们具有一个相同的抽象基类——CompositeValidator。下面提供的代码片断表明,CompositeValidator仅仅在原来的基础上增加了一个IEnumerable<Validator>类型只读属性:Validators。我将Validators集合中的每一个Validator成为构成CompositeValidator的验证器元素(ValidatorElement)

   1: public abstract class CompositeValidator:Validator
   2: {
   3:     public CompositeValidator(string messageTemplate, IEnumerable<Validator> validators):base(messageTemplate)
   4:     {
   5:         Guard.ArgumentNotNull(validators, "validators");
   6:         this.Validators = validators;
   7:     }
   8:     public IEnumerable<Validator> Validators { get; private set; }
   9: }

而AndCompositeValidator和OrCompositeValidator定义也很简单,不用多说你也能够看明白具体采用的验证逻辑。

   1: public class AndCompositeValidator: CompositeValidator
   2: {
   3:     public AndCompositeValidator(string messageTemplate,IEnumerable<Validator> validators)
   4:         : base(messageTemplate,validators)
   5:     { }
   6:  
   7:     public override ValidationError Validate(object objectToValidate)
   8:     {
   9:         foreach (var validator in this.Validators)
  10:         {
  11:             if (null != validator.Validate(objectToValidate))
  12:             {
  13:                 return this.CreateValidationError(objectToValidate);
  14:             }
  15:         }
  16:         return null;
  17:     }
  18: }
   1: public class OrCompositeValidator : CompositeValidator
   2: {
   3:     public OrCompositeValidator(string messageTemplate, IEnumerable<Validator> validators)
   4:         : base(messageTemplate,validators){ }
   5:  
   6:     public override ValidationError Validate(object objectToValidate)
   7:     {
   8:         foreach (var validator in this.Validators)
   9:         {
  10:             var validationError = validator.Validate(objectToValidate);
  11:             if (null == validationError)
  12:             {
  13:                 return null;
  14:             }
  15:         }
  16:         return this.CreateValidationError(objectToValidate);
  17:     }
  18: }

基于重用的目的,我们会CompositeValidator定义了ValidatorAttribute基类:CompositeValidatorAttribute。我们将所有ValidatorElement的名称用逗号作为分隔符连接成一个字符串参数传入到构造函数中

   1: public abstract class CompositeValidatorAttribute : ValidatorAttribute
   2: {        
   3:     public IEnumerable<string> ValidatorElements { get; private set; }
   4:     public CompositeValidatorAttribute(string messageTemplate, string validatorElements):base(messageTemplate)
   5:     {
   6:         Guard.ArgumentNotNullOrEmpty(validatorElements, "validatorElements");
   7:         this.ValidatorElements = validatorElements.Split(',');
   8:     }
   9:     public abstract CompositeValidator CreateCompositeValidator(IEnumerable<Validator> validator);
  10:     public override Validator CreateValidator()
  11:     {
  12:         throw new NotImplementedException();
  13:     }
  14: }

三、验证器元素(ValidatorElement)如何定义?

我们所有的验证规则均通过自定义特性(Attribute)的方式进行定义,说白了就是通过特性的方式将相应的Validator应用到数据类型的目标属性中去。Validator通过ValidatorAttribute可以方便地进行应用,但是构成上述CompositeValidator的验证器元素有如何应用呢?在这里我创建了另一种类型的特性——ValidatorElementAttribute。和ValidatorAttribute不同,ValidatorElementAttribute没有定义MessageTemplate属性,应为最终的验证消息通过CompositeValidator来提供。ValidatorElementAttribute提供了一个Name属性,表示该验证器元素的唯一标识。ValidatorElementAttribute和ValidatorAttribute一样,最终通过CreateValidator创建相应的Validator

   1: [AttributeUsage(AttributeTargets.Property, AllowMultiple =true)]
   2: public abstract class ValidatorElementAttribute:Attribute
   3: {
   4:     public string Name { get; private set; }
   5:     public ValidatorElementAttribute(string name )
   6:     {
   7:         Guard.ArgumentNotNullOrEmpty(name, "name");
   8:         this.Name = name;
   9:     }
  10:     public abstract Validator CreateValidator();
  11: }

四、CompositeValidator需要有特殊的ValidatorElementAttribute

对于任何一个具体的Validator,由于它既可以作为独立的验证器进行数据验证工作,也可以作为CompositeValidator的验证器元素协同其他的Validator一起完成复杂逻辑判断。所以一个Validator同时具有一个ValidatorAttribute和ValidatorElementAttribute,即使CompositeValidator本身也不能例外。原因很简单,CompositeValidator本身通过自己的ValidatorElement按照相应的逻辑判断规则进行验证,其自身也可以作为另一个CompositeValidator的ValidatorElement

但是基于CompositeValidator的ValidatorElementAttribute有点特别,以至于我们不得不为之定义一个单独的基类:CompositeValidatorElementAttribute。由于CompositeValidator具有一个以IEnumerable<Validator>对象体现的验证器元素的列表,在ValidatorElementAttribute的CreateValidator方法中无法获取,所以不得不创建一个额外的CreateCompositeValidator抽象方法,以输入参数的方式提供验证器元素列表

   1: public abstract class CompositeValidatorElementAttribute : ValidatorElementAttribute
   2: {
   3:     public IEnumerable<string> ValidatorElements { get; private set; }
   4:     public CompositeValidatorElementAttribute(string name, string validatorElements)
   5:         : base(name)
   6:     {
   7:         Guard.ArgumentNotNullOrEmpty(validatorElements, "validatorElements");
   8:         this.ValidatorElements = validatorElements.Split(',');
   9:     }
  10:     public abstract CompositeValidator CreateCompositeValidator(IEnumerable<Validator> validators);
  11:     public override Validator CreateValidator()
  12:     {
  13:         throw new NotImplementedException();
  14:     }
  15: }

以上我们着重在介绍CompositeValidator的设计,CompositeValidator以及相关的ValidatorElementAttribute和CompositeValidatorElementAttribute之间的关系可以通过下面的类图表示。

image

五、最终的验证如何进行?

到目前为止,构成验证框架的所有核心的元素都已经介绍完成,现在我们来看看最终的验证是如何进行的。在《编程篇》我们可以看到没,我们最终是调用静态外观类Validation的Validate方法对数据实体对象进行验证的。具体来说我们定义了如下两个Validate重载,其正一个可以指定验证规则名称。

   1: public static class Validation
   2: {    
   3:     public static bool Validate(object value, out IEnumerable<ValidationError> validationErrors)
   4:     {
   5:         return Validate(value,string.Empty, out validationErrors);
   6:     }
   7:  
   8:     public static bool Validate(object value, string ruleName, out IEnumerable<ValidationError> validationErrors)
   9:     {
  10:         //省略
  11:     }
  12: }

最终的验证逻辑都实现在带ruleName参数的Validate方法中,下面是该方法的定义。只要的逻辑就是:通过反射获取验证对象类型的共有PropertyInfo,并通过它和验证规则名称得到匹配的Validator的列表,然后用它们对属性的值进行验证。

   1: public static bool Validate(object value, string ruleName, out IEnumerable<ValidationError> validationErrors)
   2: {
   3:     validationErrors = new List<ValidationError>();
   4:     Guard.ArgumentNotNull(value, "value");
   5:     foreach (var property in value.GetType().GetProperties())
   6:     {
   7:         var validators = GetValidators(ruleName, property);
   8:         if (validators.Count() > 0)
   9:         {
  10:             var propertyValue = property.GetValue(value, null);
  11:             foreach (var validator in validators)
  12:             {
  13:                 validator.FormatMessage(propertyValue);
  14:                 var error = validator.Validate(propertyValue);
  15:                 if (null != error)
  16:                 {
  17:                     ((List<ValidationError>)validationErrors).Add(error);
  18:                 }
  19:             }
  20:         }
  21:     }
  22:     return validationErrors.Count() == 0;
  23: }

实际上Validate方法中最复杂的逻辑在于如何通过PropertyInfo和验证规则的名称获取匹配的Validator的列表。主要的思路还是通过PropertyInfo进行反射获取应用在上面的ValidatorAttribute,并通过得到ValidatorAttribute创建相应的Validator。不过这其中涉及到对Validator的缓存,以及的CompositeValidator创建时采用的递归,代码相对较多,在这里不作具体介绍了。有兴趣的朋友可以从这里下载源代码进行分析。

六、更多的考虑

和我很多文章一样,这篇文章仅仅是为某个应用场景提供一种大体上的思路,或者提供一种最为简单的解决方案。如果你需要将这些半理论的东西用于实践,你应该根据具体的需求做更多的考虑。本文提供的所谓的“验证框架”我之所以打上引号,是因为其粗陋不堪,实际上就是我花了不到3个小时写成的一个小程序。不过,对于这个小程序背后的解决问题的思路,我觉得还是可取的。有心的朋友不妨从下面的方面对这个“框架”进行扩充:

  • 通过配置+特性的方式是验证规则的变得更加灵活;
  • Validator不仅仅能够应用于属性,可以考虑字段;
  • Validator不仅仅能够应用公有成员,或许可以考虑非公有成员;
  • Validator不仅仅可以应用于属性、字段,也可以应用于整个类型,这就可以对整个对象级别定义验证规则,比如验证StartDate〈EndDate(StartDate和EndDate对应两个属性);
  • Validator还可以应用于方法的参数;
  • 考虑和相应AOP框架集成,让验证(主要是参数验证)自动完成;
  • 如果你希望将Validator应用于WCF服务或者契约方法的参数,可以考虑通过WCF扩展让验证工作自动执行;
  • 通过Resource的方式定义验证消息模板,可以获得多语言文化的支持
  • 其他

采用一个自创的"验证框架"实现对数据实体的验证[编程篇]
采用一个自创的"验证框架"实现对数据实体的验证[设计篇]
采用一个自创的"验证框架"实现对数据实体的验证[改进篇]
采用一个自创的"验证框架"实现对数据实体的验证[扩展篇]


作者:蒋金楠
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
前端开发 Java 关系型数据库
超市商品管理系统的设计与实现(论文+源码)_kaic
超市商品管理系统的设计与实现(论文+源码)_kaic
|
移动开发 监控 网络协议
每个端侧产品都需要的用户体验监控
ARMS RUM 是阿里云应用实时监控服务(ARMS)下的用户体验监控(RUM)产品,覆盖 Web/H5、各类平台小程序、Android、iOS、Flutter、ReactNative、Windows、macOS 等平台框架。接入 SDK 后会主动采集端侧页面性能、资源加载、API 调用、异常崩溃、卡顿、用户操作、系统信息等数据,还支持事件、日志、异常等数据按需自定义上报以满足业务数据分析需求,提供全面的性能分析、异常分析、产品分析、会话分析能力,帮助快速跟踪定位问题原因,提升产品用户使用体验。
893 111
|
机器学习/深度学习 人工智能
Qwen2VL-Flux:开源的多模态图像生成模型,支持多种生成模式
Qwen2VL-Flux 是一个开源的多模态图像生成模型,结合了 Qwen2VL 的视觉语言理解和 FLUX 框架,能够基于文本提示和图像参考生成高质量的图像。该模型支持多种生成模式,包括变体生成、图像到图像转换、智能修复及 ControlNet 引导生成,具备深度估计和线条检测功能,提供灵活的注意力机制和高分辨率输出,是一站式的图像生成解决方案。
1214 4
Qwen2VL-Flux:开源的多模态图像生成模型,支持多种生成模式
|
前端开发 JavaScript 开发者
掌握 CSS 弹性布局(Flexbox):构建复杂页面布局的高效秘籍与实战案例
CSS弹性布局(Flexbox)是现代网页设计中构建复杂页面布局的高效工具。本文将深入浅出地介绍Flexbox的核心概念、使用技巧及实际应用案例,帮助读者快速掌握这一强大布局方法。
|
JSON 算法 安全
Web安全-JWT认证机制安全性浅析
Web安全-JWT认证机制安全性浅析
273 2
|
数据管理 大数据 OLAP
AnalyticDB核心概念详解:表、索引与分区
【10月更文挑战第25天】在大数据时代,高效的数据库管理和分析工具变得尤为重要。阿里云的AnalyticDB(ADB)是一款完全托管的实时数据仓库服务,能够支持PB级数据的实时查询和分析。作为一名数据工程师,我有幸在多个项目中使用过AnalyticDB,并积累了丰富的实践经验。本文将从我个人的角度出发,详细介绍AnalyticDB的核心概念,包括表结构设计、索引类型选择和分区策略,帮助读者更有效地组织和管理数据。
521 3
|
编解码 监控 Android开发
Pico Neo 3教程☀️ 四、开发者工具:实时监控工具(Metrics Tool)
Pico Neo 3教程☀️ 四、开发者工具:实时监控工具(Metrics Tool)
|
机器学习/深度学习 人工智能 前端开发
BladeDISC 深度学习编译器问题之动态shape问题如何解决
BladeDISC 深度学习编译器问题之动态shape问题如何解决
|
算法
MATLAB | 插值算法 | 二维interp2插值法 | 附数据和出图代码 | 直接上手
MATLAB | 插值算法 | 二维interp2插值法 | 附数据和出图代码 | 直接上手
1396 0
|
前端开发 JavaScript 网络协议
集成websocket实现实时通信(ruoyi 使用笔记)
集成websocket实现实时通信(ruoyi 使用笔记)
1662 1

热门文章

最新文章