CQRS体系结构中的域验证

Jup*_*aol 45 c# architecture validation domain-driven-design cqrs

危险......危险史密斯博士......前面的哲学文章

这篇文章的目的是确定将验证逻辑放在我的域实体之外(实际上是聚合根)是否实际上给了我更大的灵活性或它是神风代码

基本上我想知道是否有更好的方法来验证我的域实体.这就是我计划这样做的方式,但我希望你的意见

我考虑的第一种方法是:

class Customer : EntityBase<Customer>
{
   public void ChangeEmail(string email)
   {
      if(string.IsNullOrWhitespace(email))   throw new DomainException(“...”);
      if(!email.IsEmail())  throw new DomainException();
      if(email.Contains(“@mailinator.com”))  throw new DomainException();
   }
}
Run Code Online (Sandbox Code Playgroud)

我实际上不喜欢这种验证,因为即使我将验证逻辑封装在正确的实体中,这也违反了Open/Close原则(Open for extension但Close for modification)我发现违反这个原则,代码维护就变成了当应用程序在复杂性中成长时,真正的痛苦.为什么?因为域规则的变化比我们想要承认的更频繁,并且如果规则被隐藏并嵌入到这样的实体中,它们很难测试,难以阅读,难以维护但是我不喜欢这个的真正原因方法是:如果验证规则发生变化,我必须来编辑我的域实体.这是一个非常简单的例子,但在RL中验证可能更复杂

因此遵循Udi Dahan的哲学,明确角色,以及Eric Evans在蓝皮书中的推荐,接下来的尝试是实现规范模式,就像这样

class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer>
{
   private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver;
   public bool IsSatisfiedBy(Customer customer)
   {
      return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email);
   }
}
Run Code Online (Sandbox Code Playgroud)

但后来我才意识到,为了遵循这种方法,我必须首先改变我的实体,以便传递值为valdiated,在这种情况下是电子邮件,但是改变它们会导致我的域事件被触发,我不想直到新电子邮件有效为止

因此,在考虑了这些方法后,我推出了这个方法,因为我要实现CQRS架构:

class EmailDomainIsAllowedValidator : IDomainInvariantValidator<Customer, ChangeEmailCommand>
{
   public void IsValid(Customer entity, ChangeEmailCommand command)
   {
      if(!command.Email.HasValidDomain())  throw new DomainException(“...”);
   }
}
Run Code Online (Sandbox Code Playgroud)

这是主要的想法,实体被传递给验证器,以防我们需要来自实体的某些值来执行验证,该命令包含来自用户的数据,并且因为验证器被认为是注入的对象,所以它们可以注入外部依赖性如果验证需要它.

现在这个困境,我对这样的设计感到满意,因为我的验证被封装在单个对象中,带来了许多优点:简单的单元测试,易于维护,使用泛在语言明确表达域不变量,易于扩展,验证逻辑是集中式和验证器可以一起使用来强制执行复杂的域规则.即使我知道我将我的实体验证放在他们之外(你可能会争辩代码气味 - 贫血领域)但我认为权衡是可以接受的

但有一件事我还没弄清楚如何以干净的方式实现它.我应该如何使用这些组件......

由于它们将被注入,它们将不能自然地适应我的域实体,所以基本上我看到两个选项:

  1. 将验证器传递给我的实体的每个方法

  2. 从外部验证我的对象(从命令处理程序)

我对选项1不满意所以我会解释如何用选项2来做

class ChangeEmailCommandHandler : ICommandHandler<ChangeEmailCommand>
{
   // here I would get the validators required for this command injected
   private IEnumerable<IDomainInvariantValidator> validators;
   public void Execute(ChangeEmailCommand command)
   {
      using (var t = this.unitOfWork.BeginTransaction())
      {
         var customer = this.unitOfWork.Get<Customer>(command.CustomerId);
         // here I would validate them, something like this
         this.validators.ForEach(x =. x.IsValid(customer, command));
         // here I know the command is valid
         // the call to ChangeEmail will fire domain events as needed
         customer.ChangeEmail(command.Email);
         t.Commit();
      }
   }
}
Run Code Online (Sandbox Code Playgroud)

好吧就是这样.您能否告诉我您对此的看法或与Domain实体验证分享您的经验

编辑

我认为我的问题并不清楚,但真正的问题是:隐藏域规则会对应用程序的未来可维护性产生严重影响,并且域规则通常会在应用程序的生命周期中发生变化.因此,考虑到这一点实现它们将让我们轻松地扩展它们.现在想象一下,未来会实现规则引擎,如果规则被封装在域实体之外,这种更改将更容易实现

我知道将验证置于我的实体之外会破坏@jgauffin在其答案中提到的封装,但我认为将验证放在单个对象中的好处远比仅保留实体的封装要大得多.现在我认为封装在传统的n层架构中更有意义,因为实体用于域层的多个位置,但在CQRS架构中,当命令到达时,将有一个命令处理程序访问聚合根和对聚合根执行操作只创建一个完美的窗口来进行验证.

我想对在实体中放置验证与将其放在单个对象中的优点进行小的比较

  • 单个对象中的验证

    • 临.容易写
    • 临.易于测试
    • 临.它明确表达了
    • 临.它成为域设计的一部分,用当前的泛在语言表达
    • 临.由于它现在是设计的一部分,因此可以使用UML图建模
    • 临.非常容易维护
    • 临.使我的实体和验证逻辑松散耦合
    • 临.易于扩展
    • 临.继SRP之后
    • 临.遵循开/关原则
    • 临.不违反得墨忒耳(mmm)的规律?
    • 临.我是集中的
    • 临.它可以重复使用
    • 临.如果需要,可以轻松注入外部依赖项
    • 临.如果使用插件模型,只需删除新的程序集即可添加新的验证程序,而无需重新编译整个应用程序
    • 临.实现规则引擎会更容易
    • CON.打破封装
    • CON.如果封装是强制性的,我们必须将各个验证器传递给实体(聚合)方法
  • 验证封装在实体内部

    • 临.封装?
    • 临.可重复使用的?

我很乐意阅读你对此的看法

Son*_*ate 11

我同意其他回复中提出的一些概念,但我将它们放在我的代码中.

首先,我同意将Value Objects用于包含行为的值是封装常见业务规则的好方法,而电子邮件地址是一个完美的候选者.但是,我倾向于将此限制为常量且不会经常更改的规则.我确信你正在寻找更通用的方法,电子邮件只是一个例子,所以我不会专注于那个用例.

我的方法的关键是认识到验证在应用程序的不同位置有不同的用途.简而言之,只验证确保当前操作可以在没有意外/意外结果的情况下执行所需的内容.这导致了一个问题,验证应该在哪里发生?

在您的示例中,我会问自己,域实体是否真的关心电子邮件地址是否符合某些模式和其他规则,还是我们只关心调用ChangeEmail时"电子邮件"不能为空或空白?如果是后者,那么在ChangeEmail方法中只需要一个简单的检查以确保存在一个值.

在CQRS中,修改应用程序状态的所有更改都作为命令在命令处理程序中执行(如您所示).我通常会将任何"挂钩"放入业务规则等,以验证操作是否可以在命令处理程序中执行.我实际上按照你的方法将验证器注入命令处理程序,这允许我扩展/替换规则集而不需要更改处理程序.这些"动态"规则允许我在更改实体状态之前定义业务规则,例如构成有效电子邮件地址的内容 - 进一步确保它不会进入无效状态.但是,在这种情况下,"无效"是由业务逻辑定义的,正如您所指出的那样,它是高度动态的.

通过CSLA排名,我发现这种变化难以采用,因为它似乎打破了封装.但是,如果您退后一步并询问验证在模型中真正起作用的角色,那么我认为封装不会被打破.

我发现这些细微差别对于让我清楚地了解这个主题非常重要.有一些验证可以防止属于方法本身的错误数据(例如缺少参数,空值,空字符串等),并且有验证可以确保强制执行业务规则.对于前者,如果客户必须有电子邮件地址,那么我唯一需要关注的规则是防止我的域对象变得无效,这是为了确保已经向我们提供了一个电子邮件地址. ChangeEmail方法.其他规则是关于价值本身有效性的更高层次的关注,并且实际上对域实体本身的有效性没有影响.

这是与其他开发人员进行大量"讨论"的根源,但当大多数人采取更广泛的观点并调查角色验证真正起作用时,他们往往会看到光明.

最后,还有一个用于UI验证的地方(并且通过UI我的意思是任何用作应用程序的界面,无论是屏幕,服务端点还是其他).我发现完全合理地复制UI中的一些逻辑以为用户提供更好的交互性.但这是因为这个验证只是为了我允许这种复制的单一目的.但是,使用注入的验证器/规范对象可以以这种方式促进重用,而不会在多个位置定义这些规则的负面影响.

不确定这是否有帮助......


pjv*_*vds 7

我不建议将大块代码扔到您的域中进行验证.我们通过将它们视为我们域中缺少概念的气味来消除大多数尴尬的放置验证.在您编写的示例代码中,我看到了对电子邮件地址的验证.客户与电子邮件验证无关.

为什么不在构造中进行一次ValueObject调用Email呢?

我的经验是,尴尬的放置验证是暗示您的域中遗漏概念的提示.您可以在Validator对象中捕获它们,但我更喜欢值对象,因为您将相关概念作为域的一部分.


Yev*_*rov 5

你把验证放在了错误的地方.

你应该使用ValueObjects来做这些事情.观看此演示文稿http://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson 它还将教您数据作为重心.

还有一个如何重用数据验证的示例,例如使用静态验证方法ala Email.IsValid(string)


Rob*_*tMS 5

我正处于项目的开始阶段,我将在我的域实体之外实现我的验证.我的域实体将包含保护任何不变量的逻辑(例如缺少参数,空值,空字符串,集合等).但实际的业务规则将存在于验证器类中.我是@SonOfPirate的心态......

我正在使用FluentValidation,它实际上会给我一堆作用于我的域实体的验证器:aka,规范模式.此外,根据Eric蓝皮书中描述的模式,我可以使用他们执行验证所需的任何数据构建验证器(无论是来自数据库还是其他存储库或服务).我也可以选择在这里注入任何依赖项.我还可以编写和重用这些验证器(例如,可以在Employee验证器和公司验证器中重用地址验证器).我有一个Validator工厂,充当"服务定位器":

public class ParticipantService : IParticipantService
{
    public void Save(Participant participant)
    {
        IValidator<Participant> validator = _validatorFactory.GetValidator<Participant>();
        var results = validator.Validate(participant);
            //if the participant is valid, register the participant with the unit of work
            if (results.IsValid)
            {
                if (participant.IsNew)
                {
                    _unitOfWork.RegisterNew<Participant>(participant);
                }
                else if (participant.HasChanged)
                {
                    _unitOfWork.RegisterDirty<Participant>(participant);
                }
            }
            else
            {
                _unitOfWork.RollBack();
                //do some thing here to indicate the errors:generate an exception (or fault) that contains the validation errors. Or return the results
            }
    }

}
Run Code Online (Sandbox Code Playgroud)

验证器将包含代码,如下所示:

   public class ParticipantValidator : AbstractValidator<Participant>
    {
        public ParticipantValidator(DateTime today, int ageLimit, List<string> validCompanyCodes, /*any other stuff you need*/)
        {...}

    public void BuildRules()
    {
             RuleFor(participant => participant.DateOfBirth)
                    .NotNull()
                    .LessThan(m_today.AddYears(m_ageLimit*-1))
                    .WithMessage(string.Format("Participant must be older than {0} years of age.", m_ageLimit));

            RuleFor(participant => participant.Address)
                .NotNull()
                .SetValidator(new AddressValidator());

            RuleFor(participant => participant.Email)
                .NotEmpty()
                .EmailAddress();
            ...
}

    }
Run Code Online (Sandbox Code Playgroud)

我们必须支持多种类型的演示:网站,winforms和通过服务批量加载数据.在固定下,所有这些都是一组服务,以单一和一致的方式公开系统的功能.我们不会使用实体框架或ORM,因为我不会厌烦您.

这就是我喜欢这种方法的原因:

  • 验证程序中包含的业务规则完全可以进行单元测试.
  • 我可以从更简单的规则中编写更复杂的规则
  • 我可以在我的系统中的多个位置使用验证器(我们支持网站和Winforms,以及公开功能的服务),因此如果服务中的用例与网站不同,则需要稍微不同的规则,那么我能解决这个问题.
  • 所有的vaildation都在一个位置表示,我可以选择注入和组成的方式/位置.


Mat*_*ore -4

您可以将基于消息的解决方案与领域事件结合使用,如此处所述

异常并不是处理所有验证错误的正确方法,并不是说无效实体是异常情况。

如果验证并不简单,则验证聚合的逻辑可以直接在服务器上执行,当您尝试设置新输入时,您可以引发域事件来告诉用户或正在使用您的域的应用程序)为什么输入不正确。