Проверка домена в архитектуре CQRS

Danger ... Danger Dr. Smith... Philosophical post ahead

Цель этого поста - определить, действительно ли размещение логики проверки вне сущностей моего домена (на самом деле, агрегатного корня) дает мне большую гибкость или этоkamikaze code

По сути, я хочу знать, есть ли лучший способ проверки сущностей моего домена. Вот как я планирую это сделать, но мне бы хотелось, чтобы ваше мнение

Первый подход, который я рассмотрел, был:

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

Мне действительно не нравится эта проверка, потому что даже когда я инкапсулирую логику проверки в правильную сущность, это нарушает принцип открытия / закрытия (открыт для расширения, но закрыт для модификации), и я обнаружил, что нарушение этого принципа приводит к поддержанию кода настоящая боль, когда приложение становится все сложнее. Зачем? Потому что правила домена меняются чаще, чем хотелось бы признать, и если правилаhidden and embedded в такой сущности их сложно протестировать, трудно прочитать, сложно поддерживать, но настоящая причина, по которой мне не нравится этот подход, заключается в следующем: если меняются правила проверки, я должен прийти и отредактировать свою сущность домена. Это был действительно простой пример, но в RL проверка может быть более сложной

Итак, следуя философии Уди Дахана,making roles explicitи рекомендация Эрика Эванса из синей книги, следующей попыткой было реализовать шаблон спецификации, что-то вроде этого

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

Но потом я понимаю, что для того, чтобы следовать этому подходу, мне сначала пришлось мутировать свои сущности, чтобы пройтиvalue being valdiatedв этом случае сообщение электронной почты, но с его изменением может привести к запуску событий моего домена, чего я бы не хотел, пока новое сообщение не станет действительным

So after considering these approaches, I came out with this one, since I am going to implement a CQRS architecture:

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

Хорошо, что это основная идея, сущность передается в валидатор, если нам требуется какое-то значение от сущности для выполнения валидации, команда содержит данные, поступающие от пользователя, и поскольку валидаторы рассматриваютсяinjectable Объекты, в которые они могут вставлять внешние зависимости, если этого требует проверка.

Now the dilemmaЯ доволен таким дизайном, потому что моя валидация заключена в отдельные объекты, что дает много преимуществ: простота модульного тестирования, простота обслуживания, инварианты домена явно выражены с использованием вездесущего языка, простота расширения, логика валидации централизована и валидаторы может использоваться вместе для обеспечения соблюдения сложных правил домена. И даже когда я знаю, что размещаю проверку своих сущностей вне их (Вы могли бы поспорить, что запах кода - Anemic Domain), но я думаю, что компромисс приемлем

Но есть одна вещь, которую я так и не понял, как правильно ее реализовать.How should I use this components...

Поскольку они будут внедрены, они не будут естественным образом вписываться в сущности моего домена, поэтому в основном я вижу два варианта:

Pass the validators to each method of my entity

Validate my objects externally (from the command handler)

Я не доволен вариантом 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();
      }
   }
}

Ну вот и все. Можете ли вы поделиться своими мыслями по этому поводу или поделиться своим опытом проверки доменных сущностей?

EDIT

Я думаю, что это не ясно из моего вопроса, но реальная проблема заключается в следующем: сокрытие правил домена имеет серьезные последствия для будущей ремонтопригодности приложения, а также правила домена часто меняются в течение жизненного цикла приложения. Следовательно, реализация их с учетом этого позволит нам легко их расширить. Теперь представьте, что в будущем будет реализован механизм правил, если правила инкапсулируются вне сущностей домена, это изменение будет легче реализовать

Я знаю, что размещение проверки вне моих сущностей нарушает инкапсуляцию, как @jgauffin упомянул в своем ответе, но я думаю, что преимущества размещения валидации в отдельных объектах гораздо более существенны, чем просто сохранение инкапсуляции сущности. Теперь я думаю, что инкапсуляция имеет больше смысла в традиционной n-уровневой архитектуре, потому что объекты использовались в нескольких местах уровня домена, но в архитектуре CQRS, когда команда прибудет, будет обработчик команд, обращающийся к совокупному корню и выполнение операций с совокупным корнем, только создав идеальное окно для размещения проверки.

Я хотел бы сделать небольшое сравнение между преимуществами размещения проверки внутри объекта по сравнению с размещением его в отдельных объектах.

Validation in Individual objects

Pro. Easy to write Pro. Easy to test Pro. It's explicitly expressed Pro. It becomes part of the Domain design, expressed with the current Ubiquitous Language Pro. Since it's now part of the design, it can be modeled using UML diagrams Pro. Extremely easy to maintain Pro. Makes my entities and the validation logic loosely coupled Pro. Easy to extend Pro. Following the SRP Pro. Following the Open/Close principle Pro. Not breaking the law of Demeter (mmm)? Pro. I'is centralized Pro. It could be reusable Pro. If required, external dependencies can be easily injected Pro. If using a plug-in model, new validators can be added just by dropping the new assemblies without the need to re-compile the whole application Pro. Implementing a rules engine would be easier Con. Breaking encapsulation Con. If encapsulation is mandatory, we would have to pass the individual validators to the entity (aggregate) method

Validation encapsulated inside the entity

Pro. Encapsulated? Pro. Reusable?

Я хотел бы прочитать ваши мысли по этому поводу

Ответы на вопрос(11)

Ваш ответ на вопрос