Language agnostic 如何将数据验证与我的简单域对象(POCO)分离?

Language agnostic 如何将数据验证与我的简单域对象(POCO)分离?,language-agnostic,separation-of-concerns,solid-principles,modular-design,Language Agnostic,Separation Of Concerns,Solid Principles,Modular Design,这个问题与语言无关,但我是个C#人,所以我使用术语POCO来表示只执行数据存储的对象,通常使用getter和setter字段 我刚刚将我的域模型修改为super duper POCO,剩下的几个问题是如何确保属性值在域中有意义 例如,服务的结束日期不应超过该服务所属合同的结束日期。但是,将检查放入Service.EndDate setter似乎违反了SOLID,更不用说随着需要进行的验证数量的增加,我的POCO类将变得杂乱无章 我有一些解决方案(将在答案中发布),但它们也有它们的缺点,我想知道解

这个问题与语言无关,但我是个C#人,所以我使用术语POCO来表示只执行数据存储的对象,通常使用getter和setter字段

我刚刚将我的域模型修改为super duper POCO,剩下的几个问题是如何确保属性值在域中有意义

例如,服务的结束日期不应超过该服务所属合同的结束日期。但是,将检查放入Service.EndDate setter似乎违反了SOLID,更不用说随着需要进行的验证数量的增加,我的POCO类将变得杂乱无章


我有一些解决方案(将在答案中发布),但它们也有它们的缺点,我想知道解决这个难题最喜欢的方法是什么?

事实上,我认为那可能是逻辑的最佳位置,但那只是我自己。您可以使用某种IsValid方法来检查所有条件并返回true/false,可能是某种ErrorMessages集合,但这是一个不确定的主题,因为错误消息实际上不是域模型的一部分。我有点偏颇,因为我对RoR做了一些工作,这基本上就是它的模型所做的。

我总是听到人们争论“验证”或“有效”方法

我个人认为这可能有效,但对于大多数DDD项目,您通常会以失败告终 根据对象的特定状态,允许进行多个验证

因此,我更喜欢“IsValidForNewContract”、“IsValidForTermination”或类似的方法,因为我相信大多数项目最终都会在每个类中使用多个这样的验证器/状态。这也意味着我没有接口,但我可以编写聚合验证器,这些验证器可以很好地反映我所断言的业务条件


我确实相信,在这种情况下,通用解决方案通常会将注意力从重要的东西上移开,即代码正在做什么,从而在技术优雅性(接口、委托或其他方面)上获得很小的收益。就投我一票吧;)

一种解决方案是让每个对象的DataAccessObject获取一个验证器列表。调用Save时,它会对每个验证器执行检查:

public class ServiceEndDateValidator : IValidator<Service> {
  public void Check(Service s) {
    if(s.EndDate > s.Contract.EndDate)
      throw new InvalidOperationException();
  }
}

public class ServiceDao : IDao<Service> {
  IValidator<Service> _validators;
  public ServiceDao(IEnumerable<IValidator<Service>> validators) {_validators = validators;}
  public void Save(Service s) {
    foreach(var v in _validators)
      v.Check(service);
    // Go on to save
  }
}
公共类ServiceEndDateValidator:IValidater{
公共作废检查(服务s){
如果(s.EndDate>s.Contract.EndDate)
抛出新的InvalidOperationException();
}
}
公共类服务dao:IDao{
IValidator_验证器;
公共服务DAO(IEnumerable validators){u validators=validators;}
公共作废保存(服务s){
foreach(var v in_验证器)
v、 支票(服务);
//继续保存
}
}

好处是非常明显的SoC,缺点是在调用Save()之前我们不会得到检查。

另一种可能性是让我的每个类都实现

public interface Validatable<T> {
  public event Action<T> RequiresValidation;
}
可验证的公共接口{
公共事件行动要求验证;
}
让每个类的每个setter在设置之前引发事件(也许我可以通过属性实现)


其优点是实时验证检查。但是messier代码,不清楚应该由谁来进行附加。

我认为您是从一个错误的假设开始的,即,您应该拥有只存储数据的对象,并且除了访问器之外没有方法。拥有对象的全部意义在于封装数据和行为。如果你有一个基本上只是结构的东西,你封装了什么行为?

在过去,我通常会将验证委托给服务本身,比如ValidationService。这在原则上仍然适用于DDD的理念

在内部,这将包含一组验证器和一组非常简单的公共方法,如Validate(),它可以返回一组错误对象

很简单,在C语言中是这样的#

公共类验证服务
{
私有IList_验证器;
公共IList验证(T objectToValidate)
{
foreach(IValidator validator in_validators)
{
收益返回验证程序.Validate(objectToValidate);
}
}
}

验证器可以添加到默认构造函数中,也可以通过其他类(如ValidationServiceFactory)注入。验证通过域对象上的代理或装饰器完成:

public class ServiceValidationProxy : Service {
  public override DateTime EndDate {
    get {return EndDate;}
    set {
      if(value > Contract.EndDate)
        throw new InvalidOperationexception();
      base.EndDate = value;
    }
  }
}
优点:即时验证。可通过IoC轻松配置


缺点:如果是代理,验证属性必须是虚拟的,如果是装饰器,所有域模型都必须基于接口。验证类最终会变得有点重——Proxy必须继承该类,而decorator必须实现所有方法。命名和组织可能会变得混乱。

我的一位同事想出了一个非常有效的想法。我们从来没有给它起过一个好名字,但我们称它为督察/法官

检查员会查看一个对象并告诉您它违反的所有规则。法官将决定如何处理此事。这种分离让我们做几件事。它让我们把所有规则放在一个地方(检查员),但我们可以有多个法官,并根据上下文选择法官

使用多个法官的一个例子是,客户必须有一个地址。这是一个标准的三层应用程序。在UI层中,法官将生成一些UI可以用来指示必须填写的字段的内容。UI法官没有抛出异常。在服务层有另一位法官。如果在保存过程中发现客户没有地址,它将抛出异常。在这一点上,你真的必须阻止事情继续进行

随着物体状态的变化,我们还有更严格的法官。这是一份保险申请,在报价过程中,允许将保单保存在不完整的文件中
public class ServiceValidationProxy : Service {
  public override DateTime EndDate {
    get {return EndDate;}
    set {
      if(value > Contract.EndDate)
        throw new InvalidOperationexception();
      base.EndDate = value;
    }
  }
}