【问题标题】:Validation strategies验证策略
【发布时间】:2011-07-06 16:02:30
【问题描述】:

我有一个包含许多类的业务模型,该模型中的一些逻辑实体由许多不同的类(父-子-孙)组成。在这些不同的类上,我定义了不变的约束,例如,复合材料应该具有 Code 的值。

我目前让每个类都实现这样的接口......

public interface IValidatable
{
    IEnumerable<ValidationError> GetErrors(string path);
}

如果未设置 Code,父级将添加一个验证错误,然后对每个子级执行 GetErrors,而该子级又会在每个孙子级上调用 GetErrors。

现在我需要为不同的操作验证不同的约束,例如

  1. 应始终检查某些约束,因为它们是不变的
  2. 当我想在根上执行操作 X 时,应该检查一些约束。
  3. 在执行 Y 操作时可能会检查一些额外的约束。

我曾考虑在 GetErrors 方法中添加一个“原因”参数,但出于某种原因,我无法完全确定这一点,这感觉不对。我也考虑过创建一个访问者并有一个具体的实现来验证 OperationX 和另一个 OperationY 但不喜欢这样,因为多个操作需要一些约束检查,但不是全部(例如 OperationX+OperationY 需要一个日期但不是 OperationZ),我不想复制检查的代码。

任何建议将不胜感激。

【问题讨论】:

    标签: c# design-patterns


    【解决方案1】:

    您在这里遇到了绝缘问题,因为您的类负责进行自己的验证,但验证的性质取决于您正在执行的操作类型。这意味着类需要知道可以对它们执行的操作类型,这会在类和使用它们的操作之间产生相当紧密的耦合。

    一种可能的设计是创建一组并行的类,如下所示:

    public interface IValidate<T>
    {
        IEnumerable<ValidationError> GetErrors(T instance, string path);
    }
    
    public sealed class InvariantEntityValidator : IValidate<Entity>
    {
        public IEnumerable<ValidationError> GetErrors(Entity entity, string path)
        {
            //Do simple (invariant) validation...
        }
    }
    
    public sealed class ComplexEntityValidator : IValidate<Entity>
    {
        public IEnumerable<ValidationError> GetErrors(Entity entity, string path)
        {
            var validator = new InvariantEntityValidator();
            foreach (var error in validator.GetErrors(entity, path))
                yield return error;
    
            //Do additional validation for this complex case
        }
    }
    

    您仍然需要确定如何将验证类与正在验证的各种类相关联。听起来这应该以某种方式发生在操作级别,因为这是您知道需要进行哪种类型的验证的地方。如果不更好地了解您的架构,就很难说。

    【讨论】:

      【解决方案2】:

      我会做一种基于属性的验证:

      public class Entity
      {
          [Required, MaxStringLength(50)]
          public string Property1 { get; set; }  
      
          [Between(5, 20)]
          public int Property2 { get; set; }
      
          [ValidateAlways, Between(0, 5)]
          public int SomeOtherProperty { get; set; }      
      
          [Requires("Property1, Property2")]
          public void OperationX()
          {
          }
      }
      

      传递给Requires-attribute 的每个属性都需要有效才能执行操作。

      具有ValidateAlways-attribute 的属性必须始终有效——无论何种操作。

      在我的伪代码Property1中,Property2 SomeOtherProperty必须有效才能执行OperationX

      当然,您还必须向 Requires-attribute 添加一个选项来检查子项的验证属性。但是如果没有看到一些示例代码,我无法建议如何做到这一点。

      也许是这样的:

      [Requires("Property1, Property2, Child2: Property3")]
      

      如果需要,您还可以使用 lambda 表达式而不是字符串 (Example) 来访问强类型属性指针。

      【讨论】:

      • 这是一种经常实现的方法(在我的各种项目过程中,我至少为此创建了两个变体),但它可能会变得非常复杂,非常快。需要注意的一件事是,属性只能将编译时常量作为参数传递,因此不能使用 lambda 表达式。
      • 好点,丹!是的,在使用这种方法时,这确实是一个缺点。但这取决于验证系统有什么要求。如果只需要编译时常量,这不是问题。
      • 这是一个适合 invariant 约束的 awesome。我会将其与剩余约束的代码合同结合起来。
      • 如果他想对单个实体上的不同操作进行单独验证,则数据注释不起作用
      • @JoelCoehoorn 我很想看到这种方法的示例,以了解您倾向于如何实施它。提前致谢!
      【解决方案3】:

      我建议使用Fluent Validation For .Net 库。该库允许您非常轻松灵活地设置验证器,如果您需要对不同操作进行不同验证,您可以非常轻松地使用适用于该特定操作的验证器(并更改它们)。

      【讨论】:

        【解决方案4】:

        出于完全相同的原因,我使用了 Spring.NET 的验证引擎 - 它允许您使用 条件验证器。您只需定义规则——应用什么验证以及在什么条件下,其余的由 Spring 完成。好在你的业务逻辑不再被验证接口污染

        您可以在documentation at springframework.net 中找到更多信息,我将复制他们的文档的示例以显示它的外观:

        &lt;v:condition test="StartingFrom.Date &gt;= DateTime.Today" when="StartingFrom.Date != DateTime.MinValue"&gt;

        &lt;v:message id="error.departureDate.inThePast" providers="departureDateErrors, validationSummary"/&gt;

        &lt;/v:condition&gt;

        在此示例中,比较 Trip 对象的 StartingFrom 属性以查看它是否晚于当前日期,即 DateTime 但仅在已设置日期时(StartingFrom.Date 的初始值设置为 DateTime.MinValue )。

        条件验证器可以被认为是“所有验证器之母”。您可以使用它来实现使用其他验证器类型可以实现的几乎任何事情,但在某些情况下,测试表达式可能非常复杂,这就是为什么您应该尽可能使用更具体的验证器类型。但是,如果您需要检查特定值是否属于特定范围或执行类似的测试,条件验证器仍然是您的最佳选择,因为这些条件相当容易编写。

        【讨论】:

          【解决方案5】:

          如果您使用的是 .Net 4.0,则可以使用 Code Contracts 来控制其中的一部分。

          【讨论】:

            【解决方案6】:

            尝试从头到尾阅读这篇文章,我从中获得了不少想法。

            http://codebetter.com/jeremymiller/2007/06/13/build-your-own-cab-part-9-domain-centric-validation-with-the-notification-pattern/

            它是基于属性的域验证,带有将这些验证包装到更高层的通知。

            【讨论】:

              【解决方案7】:

              我会将变体验证逻辑分开,也许像您提到的那样使用访问者。通过将验证与类分开,您将保持操作与数据分开,这确实有助于保持清洁。

              也可以这样想——如果您将验证和操作与数据类混合在一起,想想一年后的情况,当您进行增强并且需要添加一个新的操作。如果每个操作的验证规则和操作逻辑是独立的,那么它在很大程度上只是一个“添加”——您创建一个新操作,并为它创建一个新的验证访问者。另一方面,如果您必须返回并在每个数据类中触及大量“if operation == x”逻辑,那么您会增加一些回归错误/等的风险。

              【讨论】:

                【解决方案8】:

                我依赖已在 .net 框架 中的 'System.ComponentModel.DataAnnotations Namespace' 中实现的经过验证的验证策略,例如在 ASP.NET MVC 中使用。

                这提供了应用验证规则(使用属性或实现 IValidatableObject)和工具来验证规则的不显眼的方式。

                Scott Allen 在伟大的文章“Manual Validation with Data Annotations”中这样描述。

                【讨论】:

                  猜你喜欢
                  • 2011-01-17
                  • 2012-09-26
                  • 2023-03-31
                  • 2020-07-03
                  • 1970-01-01
                  • 2019-06-12
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多