一种常见的面向对象的验证技术是将验证规则建模为一等对象:
- 定义用于验证特定类型数据的通用接口
- 实现符合该接口的类或函数的集合
- 循环遍历此集合中的每个函数/对象并调用验证方法。返回值是真/假,或者可能是描述验证失败的对象(如果验证规则通过,则返回 null)。在迭代规则集合时构建验证失败列表
- 以适当的方式向用户呈现验证失败
您会看到许多图书馆都在使用这种技术。
例子:
// the entity you want to validate
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
}
public class ValidationFailure
{
public ValidationFailure(string description) { Description = description; }
public string Description { get; set; }
// perhaps add other properties here if desired
}
// note that this is generic and can be reused for any object to validate
public interface IValidationRule<TEntity>
{
ValidationFailure Test(TEntity entity);
}
public class ValidatesMaxAge : IValidationRule<Person>
{
public ValidationFailure Test(Person entity)
{
if (entity.Age > 100) return new ValidationFailure("Age is too high.");
}
}
public class ValidatesName : IValidationRule<Person>
{
public ValidationFailure Test(Person entity)
{
if (string.IsNullOrWhiteSpace(entity.Name))
return new ValidationFailure("Name is required.");
}
}
// to perform your validation
var rules = new List<IValidationRule> { new ValidatesMaxAge(), new ValidatesName() };
// test each validation rule and collect a list of failures
var failures = rules.Select(rule => rule.Test(person))
.Where(failure => failure != null);
bool isValid = !failures.Any();
这种设计的优点:
- 符合接口将促进代码模式的一致性
- 每个验证规则一个类或一个函数使您的规则保持原子性、可读性、自记录性、可重用性
- 遵循验证规则类的单一职责原则,并有助于简化需要执行验证的代码
- 降低圈复杂度(更少的嵌套
if
语句),因为它是一种更面向对象或功能性的方法,而不是程序性方法
- 允许为单个验证规则类引入依赖注入,这在您访问数据库或服务类进行验证时很有用
编辑:@Mgetz 提到了输入验证与业务验证,这也是一个重要的考虑因素。上述按规则分类的方法基于我每天使用的系统。我们更多地将其用于服务类中的业务逻辑验证(这是一个具有许多业务规则的复杂企业系统),并且该设计很好地服务于我们的目的。我上面写的具体规则非常简单,看起来更像是输入验证。对于输入验证,我建议使用更轻量级的方法,并在适当时将其与业务逻辑验证分开。
这种设计的另一种实现是每个实体有一个验证器类,并使用更轻量级的东西,例如 lambdas 用于单独的验证规则。例如,流行的Fluent Validation库使用它。这对于用户输入验证非常有用,因为它允许执行简单验证的代码更少,并鼓励您将输入验证与业务逻辑验证分开:
// Example using the FluentValidation library
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(p => p.Age).LessThan(100);
RuleFor(p => p.Name).NotEmpty();
}
}