49

危险......危险史密斯博士......前面的哲学帖子

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

基本上我想知道是否有更好的方法来验证我的域实体。这就是我打算这样做的方式,但我想听听你的意见

我考虑的第一种方法是:

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();
   }
}

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

所以遵循 Udi Dahan 的哲学,明确角色,以及蓝皮书中 Eric Evans 的建议,接下来的尝试是实现规范模式,像这样

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

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

所以在考虑了这些方法之后,我提出了这个,因为我要实现一个 CQRS 架构:

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

这就是主要思想,实体被传递给验证器,以防我们需要来自实体的一些值来执行验证,命令包含来自用户的数据,并且由于验证器被认为是可注入对象,它们可以注入外部依赖项如果验证需要它。

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

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

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

  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();
      }
   }
}

好吧,就是这样。你能告诉我你对此的看法或分享你在域实体验证方面的经验吗

编辑

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

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

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

  • 单个对象中的验证

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

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

我很想看看你对此的看法

4

11 回答 11

13

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

首先,我同意将值对象用于包含行为的值是封装常见业务规则的好方法,并且电子邮件地址是一个完美的候选者。但是,我倾向于将其限制为恒定且不会经常更改的规则。我确定您正在寻找一种更通用的方法,而电子邮件只是一个示例,因此我不会专注于那个用例。

我的方法的关键是认识到验证在应用程序的不同位置服务于不同的目的。简而言之,仅验证确保当前操作可以执行而不会出现意外/意外结果所需的内容。这就引出了一个问题,应该在哪里进行什么验证?

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

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

在通过 CSLA 排名后,我发现这种更改很难采用,因为它似乎确实打破了封装。但是,我认为,如果您退后一步并询问模型中真正的角色验证是什么,那么封装并没有被破坏。

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

这一直是与其他开发人员进行大量“讨论”的来源,但是当大多数人采取更广泛的观点并调查角色验证真正发挥作用时,他们往往会看到曙光。

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

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

于 2012-06-11T13:31:01.233 回答
8

我不建议将大量代码放入您的域中进行验证。我们消除了大部分尴尬的验证,将它们视为我们领域中缺失概念的味道。在您编写的示例代码中,我看到了对电子邮件地址的验证。客户与电子邮件验证没有任何关系。

为什么不在构造时进行ValueObjectEmail验证?

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

于 2012-06-06T06:08:37.573 回答
6

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

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

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

}

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

   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();
            ...
}

    }

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

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

  • 验证器中包含的业务规则是完全可单元测试的。
  • 我可以从更简单的规则组成更复杂的规则
  • 我可以在我的系统中的多个位置使用验证器(我们支持网站和 Winform,以及公开功能的服务),因此,如果服务中的用例需要与网站不同的用例略有不同的规则,那么我可以处理。
  • 所有验证都在一个位置表示,我可以选择如何/在哪里注入和组合它。
于 2012-06-12T15:05:28.943 回答
5

你把验证放在错误的地方。

您应该将 ValueObjects 用于此类事情。观看此演示http://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson 它还将教您有关数据作为重心的知识。

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

于 2012-06-06T19:44:10.303 回答
2

我不会调用从EntityBase我的域模型继承的类,因为它将它耦合到您的持久层。但那只是我的个人意见。

我不会将电子邮件验证逻辑从 移动Customer到其他任何东西以遵循打开/关闭原则。对我来说,遵循打开/关闭意味着您具有以下层次结构:

public class User
{
    // some basic validation
    public virtual void ChangeEmail(string email);
}

public class Employee : User
{
    // validates internal email
    public override void ChangeEmail(string email);
}

public class Customer : User
{
    // validate external email addresses.
    public override void ChangeEmail(string email);
}

您的建议将控件从域模型移动到任意类,从而破坏了封装。我宁愿重构我的类 ( Customer) 以符合新的业务规则,而不是这样做。

使用领域事件来触发系统的其他部分以获得更松耦合的架构,但不要使用命令/事件来违反封装。

例外

我刚刚注意到你扔了DomainException. 这是通用异常的一种方式。你为什么不使用参数异常或FormatException?他们更好地描述了错误。并且不要忘记包含上下文信息,以帮助您防止将来出现异常。

更新

将逻辑放在课堂之外是自找麻烦恕我直言。您如何控制使用哪个验证规则?验证时可能会使用一部分代码SomeVeryOldRule,而另一部分使用NewAndVeryStrictRule. 它可能不是故意的,但是当代码库增长时它可以并且将会发生。

听起来您已经决定忽略 OOP 基础之一(封装)。继续使用通用/外部验证框架,但不要说我没有警告你;)

更新2

感谢您的耐心和您的回答,这就是我发布这个问题的原因,我觉得一个实体应该有责任保证它处于有效状态(我在以前的项目中已经做到了)但是放置它的好处在单个对象中是巨大的,就像我发布的那样,甚至有一种方法可以使用单个对象并保持封装,但我个人对设计不太满意,但另一方面它也不是不在表中,考虑这个 ChangeEmail(IEnumerable> 验证器, string email) 我没有详细想过imple。尽管

这允许程序员指定任何规则,它可能是也可能不是当前正确的业务规则。开发人员可以写

customer.ChangeEmail(new IValidator<Customer>[] { new NonValidatingRule<Customer>() }, "notAnEmail")

它接受一切。ChangeEmail并且必须在每个被调用的地方指定规则。

如果要使用规则引擎,请创建单例代理:

public class Validator
{
    IValidatorEngine _engine;

    public static void Assign(IValidatorEngine engine)
    {
        _engine = engine;
    }

    public static IValidatorEngine Current { get { return _engine; } }
}

..并从域模型方法中使用它,例如

public class Customer
{
    public void ChangeEmail(string email)
    {
        var rules = Validator.GetRulesFor<Customer>("ChangeEmail");
        rules.Validate(email);

        // valid
    }

}

该解决方案的问题在于,由于隐藏了规则依赖关系,它将成为维护的噩梦。除非您测试每个域模型方法和每个方法的每个规则场景,否则您永远无法判断是否所有规则都已指定并有效。

该解决方案更灵活,但恕我直言,实施比重构业务规则已更改的方法需要更多时间。

于 2012-06-04T10:16:58.233 回答
2

我不能说我所做的事情是完美的,因为我自己仍在努力解决这个问题,并且一次打一场。但到目前为止,我一直在做以下事情:

我有封装验证的基本类:

public interface ISpecification<TEntity> where TEntity : class, IAggregate
    {
        bool IsSatisfiedBy(TEntity entity);
    }

internal class AndSpecification<TEntity> : ISpecification<TEntity> where TEntity: class, IAggregate
    {
        private ISpecification<TEntity> Spec1;
        private ISpecification<TEntity> Spec2;

        internal AndSpecification(ISpecification<TEntity> s1, ISpecification<TEntity> s2)
        {
            Spec1 = s1;
            Spec2 = s2;
        }

        public bool IsSatisfiedBy(TEntity candidate)
        {
            return Spec1.IsSatisfiedBy(candidate) && Spec2.IsSatisfiedBy(candidate);
        }


    }

    internal class OrSpecification<TEntity> : ISpecification<TEntity> where TEntity : class, IAggregate
    {
        private ISpecification<TEntity> Spec1;
        private ISpecification<TEntity> Spec2;

        internal OrSpecification(ISpecification<TEntity> s1, ISpecification<TEntity> s2)
        {
            Spec1 = s1;
            Spec2 = s2;
        }

        public bool IsSatisfiedBy(TEntity candidate)
        {
            return Spec1.IsSatisfiedBy(candidate) || Spec2.IsSatisfiedBy(candidate);
        }
    }

    internal class NotSpecification<TEntity> : ISpecification<TEntity> where TEntity : class, IAggregate
    {
        private ISpecification<TEntity> Wrapped;

        internal NotSpecification(ISpecification<TEntity> x)
        {
            Wrapped = x;
        }

        public bool IsSatisfiedBy(TEntity candidate)
        {
            return !Wrapped.IsSatisfiedBy(candidate);
        }
    }

    public static class SpecsExtensionMethods
    {
        public static ISpecification<TEntity> And<TEntity>(this ISpecification<TEntity> s1, ISpecification<TEntity> s2) where TEntity : class, IAggregate
        {
            return new AndSpecification<TEntity>(s1, s2);
        }

        public static ISpecification<TEntity> Or<TEntity>(this ISpecification<TEntity> s1, ISpecification<TEntity> s2) where TEntity : class, IAggregate
        {
            return new OrSpecification<TEntity>(s1, s2);
        }

        public static ISpecification<TEntity> Not<TEntity>(this ISpecification<TEntity> s) where TEntity : class, IAggregate
        {
            return new NotSpecification<TEntity>(s);
        }
    }

并使用它,我执行以下操作:

命令处理程序:

 public class MyCommandHandler :  CommandHandler<MyCommand>
{
  public override CommandValidation Execute(MyCommand cmd)
        {
            Contract.Requires<ArgumentNullException>(cmd != null);

           var existingAR= Repository.GetById<MyAggregate>(cmd.Id);

            if (existingIntervento.IsNull())
                throw new HandlerForDomainEventNotFoundException();

            existingIntervento.DoStuff(cmd.Id
                                , cmd.Date
                                ...
                                );


            Repository.Save(existingIntervento, cmd.GetCommitId());

            return existingIntervento.CommandValidationMessages;
        }

聚合:

 public void DoStuff(Guid id, DateTime dateX,DateTime start, DateTime end, ...)
        {
            var is_date_valid = new Is_dateX_valid(dateX);
            var has_start_date_greater_than_end_date = new Has_start_date_greater_than_end_date(start, end);

        ISpecification<MyAggregate> specs = is_date_valid .And(has_start_date_greater_than_end_date );

        if (specs.IsSatisfiedBy(this))
        {
            var evt = new AgregateStuffed()
            {
                Id = id
                , DateX = dateX

                , End = end        
                , Start = start
                , ...
            };
            RaiseEvent(evt);
        }
    }

该规范现在嵌入在这两个类中:

public class Is_dateX_valid : ISpecification<MyAggregate>
    {
        private readonly DateTime _dateX;

        public Is_data_consuntivazione_valid(DateTime dateX)
        {
            Contract.Requires<ArgumentNullException>(dateX== DateTime.MinValue);

            _dateX= dateX;
        }

        public bool IsSatisfiedBy(MyAggregate i)
        {
            if (_dateX> DateTime.Now)
            {
                i.CommandValidationMessages.Add(new ValidationMessage("datex greater than now"));
                return false;
            }

            return true;
        }
    }

    public class Has_start_date_greater_than_end_date : ISpecification<MyAggregate>
    {
        private readonly DateTime _start;
        private readonly DateTime _end;

        public Has_start_date_greater_than_end_date(DateTime start, DateTime end)
        {
            Contract.Requires<ArgumentNullException>(start == DateTime.MinValue);
            Contract.Requires<ArgumentNullException>(start == DateTime.MinValue);

            _start = start;
            _end = end;
        }

        public bool IsSatisfiedBy(MyAggregate i)
        {
            if (_start > _end)
            {
                i.CommandValidationMessages.Add(new ValidationMessage(start date greater then end date"));
                return false;
            }

            return true;
        }
    }

这允许我为不同的聚合重用一些验证,并且很容易测试。如果您看到其中有任何流动。我真的很乐意讨论它。

你的,

于 2012-06-04T13:42:13.970 回答
1

根据我的 OO 经验(我不是 DDD 专家),将代码从实体移动到更高的抽象级别(进入命令处理程序)会导致代码重复。这是因为每次命令处理程序获取电子邮件地址时,它都必须实例化电子邮件验证规则。这种代码过一会就会腐烂,而且闻起来很臭。在当前示例中,如果您没有另一个更改电子邮件地址的命令,它可能不会,但在其他情况下,它肯定会......

如果您不想将规则移回较低的抽象级别,例如实体或电子邮件值对象,那么我强烈建议您通过对规则进行分组来减轻痛苦。因此,在您的电子邮件示例中,以下 3 条规则:

  if(string.IsNullOrWhitespace(email))   throw new DomainException(“...”);
  if(!email.IsEmail())  throw new DomainException();
  if(email.Contains(“@mailinator.com”))  throw new DomainException();

可以是EmailValidationRule您可以更轻松地重复使用的组的一部分。

从我的角度来看,对于将验证逻辑放在哪里的问题没有明确的答案。它可以是每个对象的一部分,具体取决于抽象级别。在您当前的情况下,电子邮件地址的正式检查可以是一个的一部分,EmailValueObject并且该mailinator规则可以是更高抽象级别概念的一部分,其中您声明您的用户不能拥有指向该域的电子邮件地址。例如,如果有人想在没有注册的情况下与您的用户联系,那么您可以根据正式验证检查她的电子邮件,但您不必根据mailinator规则检查她的电子邮件。等等...

所以我完全同意@pjvds 的观点,他声称这种尴尬的放置验证是糟糕设计的标志。我认为打破封装不会有任何好处,但这是你的选择,也是你的痛苦。

于 2014-09-25T03:02:07.970 回答
0

您示例中的验证是验证值对象,而不是实体(或聚合根)。

我会将验证分为不同的区域。

  1. 在内部验证Email值对象的内部特征。

我遵守聚合永远不应处于无效状态的规则。我将此原则扩展到可行的值对象。

用于createNew()从用户输入实例化电子邮件。这会强制它根据您当前的规则(例如“user@email.com”格式)有效。

用于createExisting()实例化来自持久存储的电子邮件。这不执行验证,这很重要 - 您不希望为昨天有效但今天无效的存储电子邮件引发异常。

class Email
{
    private String value_;

    // Error codes
    const Error E_LENGTH = "An email address must be at least 3 characters long.";
    const Error E_FORMAT = "An email address must be in the 'user@email.com' format.";

    // Private constructor, forcing the use of factory functions
    private Email(String value)
    {
        this.value_ = value;
    }

    // Factory functions
    static public Email createNew(String value)
    {
        validateLength(value, E_LENGTH);
        validateFormat(value, E_FORMAT);
    }

    static public Email createExisting(String value)
    {
        return new Email(value);
    }

    // Static validation methods
    static public void validateLength(String value, Error error = E_LENGTH)
    {
        if (value.length() < 3)
        {
            throw new DomainException(error);
        }
    }

    static public void validateFormat(String value, Error error = E_FORMAT)
    {
        if (/* regular expression fails */)
        {
            throw new DomainException(error);
        }
    }

}
  1. 在外部验证值对象的“外部”特征Email,例如在服务中。

    class EmailDnsValidator implements IEmailValidator
    {
        const E_MX_MISSING = "The domain of your email address does not have an MX record.";
    
        private DnsProvider dnsProvider_;
    
        EmailDnsValidator(DnsProvider dnsProvider)
        {
            dnsProvider_ = dnsProvider;
        }
    
        public void validate(String value, Error error = E_MX_MISSING)
        {
            if (!dnsProvider_.hasMxRecord(/* domain part of email address */))
            {
                throw new DomainException(error);
            }
        }
    }
    
    class EmailDomainBlacklistValidator implements IEmailValidator
    {
        const Error E_DOMAIN_FORBIDDEN = "The domain of your email address is blacklisted.";
    
        public void validate(String value, Error error = E_DOMAIN_FORBIDDEN)
        {
            if (/* domain of value is on the blacklist */))
            {
                throw new DomainException(error);
            }
        }
    }
    

优点:

  • 使用createNew()createExisting()工厂函数允许控制内部验证。

  • 可以直接使用验证方法“选择退出”某些验证例程,例如跳过长度检查。

  • 也可以“退出”外部验证(DNS MX 记录和域黑名单)。例如,我参与的一个项目最初验证了域的 MX 记录的存在,但由于使用“动态 IP”类型解决方案的客户数量众多,最终将其删除。

  • 很容易查询您的持久存储中不符合当前验证规则的电子邮件地址,但运行一个简单的查询并将每封电子邮件视为“新”而不是“现有” - 如果抛出异常,就会出现问题。例如,您可以从那里发出FlagCustomerAsHavingABadEmail命令,使用异常错误消息作为用户看到消息时的指导。

  • 允许程序员提供错误代码提供了灵活性。例如,发送UpdateEmailAddress命令时,“您的电子邮件地址必须至少为 3 个字符长”的错误是不言自明的。但是,当更新多个电子邮件地址(家庭和工作)时,上述错误消息并不表示哪个电子邮件是错误的。提供错误代码/消息可以让您向最终用户提供更丰富的反馈。

于 2016-05-20T04:46:22.827 回答
0

不久前我写了一篇关于这个主题的博客文章。这篇文章的前提是有不同类型的验证。我称它们为表面验证和基于域的命令验证。

这个简单的版本是这样的。验证“是不是一个数字”或“电子邮件地址”之类的东西往往不仅仅是肤浅的。这些可以在命令到达域实体之前完成。

但是,如果验证与域更相关,那么它的正确位置就在域中。例如,您可能对某辆卡车可以装载的货物的重量和类型有一些规定。这听起来更像是领域逻辑。

然后你有混合类型。诸如基于集合的验证之类的事情。这些需要在命令发出或注入域之前发生(尽可能避免这种情况 - 限制依赖关系是一件好事)。

无论如何,您可以在此处阅读完整的帖子:如何在 CQRS 应用程序中验证命令

于 2017-02-17T09:56:09.113 回答
0

我仍在尝试这个概念,但你可以尝试装饰器。如果您使用 SimpleInjector,您可以轻松地注入您自己的在命令处理程序之前运行的验证类。然后该命令可以假设它到达那么远是有效的。但是,这意味着所有验证都应该在命令而不是实体上完成。实体不会进入无效状态。但是每个命令都必须完全实现自己的验证,因此类似的命令可能具有重复的规则,但您可以抽象通用规则以共享或将不同的命令视为真正独立的。

于 2017-07-02T12:37:53.887 回答
-4

您可以将基于消息的解决方案与域事件一起使用,如此所述。

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

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

于 2012-06-04T10:06:25.657 回答