验证策略

本文关键字:策略 验证 | 更新日期: 2023-09-27 18:01:15

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

我现在让每个类实现一个接口,像这样…

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

如果没有设置Code,父级将添加一个验证错误,然后在每个子级上执行GetErrors,然后在每个孙子级上调用GetErrors。

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

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

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

如有任何建议,不胜感激。

验证策略

这里有一个隔离问题,因为您的类负责执行它们自己的验证,但是该验证的性质取决于您正在执行的操作的类型。这意味着类需要知道可以对它们执行的操作的种类,这在类和使用它们的操作之间创建了相当紧密的耦合。

一种可能的设计是像这样创建一个并行的类集:

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

您仍然需要解决如何将验证类与被验证的各种类关联起来。听起来这应该以某种方式发生在操作级别,因为这是您知道需要发生哪种类型的验证的地方。如果没有更好地了解您的体系结构,这很难说。

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

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 -属性的每个属性都需要有效才能执行操作。

具有ValidateAlways -属性的属性必须始终有效-无论进行什么操作。

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

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

可能是这样的:

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

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

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

我使用了spring。它允许你使用条件验证器。您只需定义规则——应用什么验证以及在什么条件下应用,其余的由Spring完成。好处是您的业务逻辑不再受到验证接口的污染

你可以在springframework.net的文档中找到更多信息,我只是复制他们的文档中的示例来展示它的样子:

<v:condition test="StartingFrom.Date >= DateTime.Today" when="StartingFrom.Date != DateTime.MinValue">

<v:message id="error.departureDate.inThePast" providers="departureDateErrors, validationSummary"/>

</v:condition>

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

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

如果你使用的是。net 4.0,你可以使用代码契约来控制这些

试着从上到下阅读这篇文章,我从中获得了不少想法。

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

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

我会将变量验证逻辑分离出来,也许像您提到的那样使用Visitor。通过将验证与类分离,您将使操作与数据分离,这确实有助于保持整洁。

也可以这样想——如果你在数据类中混合了验证和操作,想想一年后,当你在做一个增强,你需要添加一个新的操作时,事情会是什么样子。如果每个操作的验证规则和操作逻辑是分开的,那么很大程度上只是一个"添加"——您创建了一个新的操作,并创建了一个新的验证访问者。另一方面,如果您必须返回并在每个数据类中触摸大量"if操作== x"逻辑,那么您就有一些回归错误等的额外风险。

我依赖于在.net框架在'System.ComponentModel.DataAnnotations命名空间'中实现的验证策略,例如在ASP中使用的验证策略。净MVC。

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

Scott Allen在一篇很棒的文章"Manual Validation with Data Annotations"中描述了这种方法。

  • 如果你想在接口上应用验证属性,然后查看MetadataTypeAttribute
  • 要更好地理解它是如何工作的,请查看MS源代码