【问题标题】:How to separate data validation from my simple domain objects (POCOs)?如何将数据验证与我的简单域对象 (POCO) 分开?
【发布时间】:2009-01-03 18:05:35
【问题描述】:

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

我刚刚将我的域模型重新设计为超级骗子 POCO,并且对于如何确保属性值在域中有意义存在一些问题。

例如,服务的结束日期不应超过服务所依据的合同的结束日期。但是,将检查放入 Service.EndDate 设置器似乎违反了 SOLID,更不用说随着需要完成的验证数量的增加,我的 POCO 类将变得混乱。

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

【问题讨论】:

    标签: language-agnostic separation-of-concerns solid-principles modular-design


    【解决方案1】:

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

    【讨论】:

    • 对,这些对象不封装行为。这就是重点。他们代表我的领域。这就对了。行为在别处提供。
    • 域模型没有行为。行为分别附加到它们。服务使用模型做事。至少这是我对整个 DDD 的理解。
    • 您所描述的设计被称为“贫血领域模型”。领域驱动设计可以说是面向对象设计的一种特定应用,它规定了完全相反的情况,即一个对象应该封装它正在建模的数据和行为。
    【解决方案2】:

    我总是听到人们争论“Validate”或“IsValid”方法。

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

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

    我确实相信,在这种情况下,通用解决方案通常会将注意力从重要的事情(代码正在做什么)上转移到技术优雅上(接口、委托或其他方面)的微小收益)。投票给我吧;)

    【讨论】:

      【解决方案3】:

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

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

      使用多个法官的一个例子是围绕客户必须有地址的规则展开​​的。这是一个标准的三层应用程序。在 UI 层,Judge 会生成一些 UI 可以用来指示必须填写的字段的内容。UI Judge 没有抛出异常。在服务层有另一个法官。如果它在保存期间发现没有地址的客户,它将引发异常。到那时你真的必须阻止事情继续进行。

      随着物品状态的变化,我们也有更严格的裁判。这是一份保险申请,在报价过程中,允许将保单保存为不完整的状态。但是,一旦该策略准备好激活,就必须设置很多东西。所以服务端的报价法官没有激活法官那么严格。然而,Inspector 中使用的规则仍然相同,因此即使您决定不做任何事情,您仍然可以分辨出哪些是不完整的。

      【讨论】:

        【解决方案4】:

        一种解决方案是让每个对象的 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
          }
        }
        

        好处,很清楚 SoC,坏处是直到调用 Save() 才得到检查。

        【讨论】:

          【解决方案5】:

          过去,我通常将验证委托给它自己的服务,例如 ValidationService。原则上,这仍然符合 DDD 的理念。

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

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

          public class ValidationService<T>
          {
            private IList<IValidator> _validators;
          
            public IList<Error> Validate(T objectToValidate)
            {
              foreach(IValidator validator in _validators)
              {
                yield return validator.Validate(objectToValidate);
              }
            }
          }
          

          验证器可以添加到默认构造函数中,也可以通过其他类(例如 ValidationServiceFactory)注入。

          【讨论】:

          【解决方案6】:

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

          【讨论】:

          • SRP - 你的模型是负责表示现实世界的对象还是负责验证输入?在这方面必须无情
          【解决方案7】:

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

          public interface Validatable<T> {
            public event Action<T> RequiresValidation;
          }
          

          并让每个类的每个设置器在设置之前引发事件(也许我可以通过属性来实现)。

          优点是实时验证检查。但是代码比较混乱,不清楚应该由谁来做附件。

          【讨论】:

            【解决方案8】:

            这是另一种可能性。验证是通过域对象上的代理或装饰器完成的:

            public class ServiceValidationProxy : Service {
              public override DateTime EndDate {
                get {return EndDate;}
                set {
                  if(value > Contract.EndDate)
                    throw new InvalidOperationexception();
                  base.EndDate = value;
                }
              }
            }
            

            优点:即时验证。可以通过 IoC 轻松配置。

            缺点:如果是代理,经过验证的属性必须是虚拟的,如果是装饰器,所有域模型都必须是基于接口的。验证类最终会有点重量级——代理必须继承类,装饰器必须实现所有方法。命名和组织可能会令人困惑。

            【讨论】:

              猜你喜欢
              • 2013-07-26
              • 1970-01-01
              • 2011-07-03
              • 2011-01-23
              • 1970-01-01
              • 2012-11-24
              • 2012-08-27
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多