Domain Validation in a CQRS architecture

Perigo ... Perigo Dr. Smith ... Post filosófico à frente

O objetivo deste post é determinar se colocar a lógica de validação fora de minhas entidades de domínio (raiz agregada, na verdade) está realmente me concedendo mais flexibilidade ou écódigo kamikaze

Basicamente, quero saber se existe uma maneira melhor de validar minhas entidades de domínio. É assim que estou planejando fazer isso, mas eu gostaria que sua opinião

A primeira abordagem que considerei foi:

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

Eu realmente não gosto dessa validação porque mesmo quando eu estou encapsulando a lógica de validação na entidade correta, isso está violando o princípio Open / Close (Open for extension mas Close for modification) e descobri que violando este princípio, a manutenção do código se torna uma dor real quando o aplicativo cresce em complexidade. Por quê? Porque as regras do domínio mudam mais frequentemente do que gostaríamos de admitir, e se as regras sãooculto e incorporado em uma entidade como essa, eles são difíceis de testar, difíceis de ler, difíceis de manter, mas a verdadeira razão pela qual eu não gosto dessa abordagem é: se as regras de validação mudarem, preciso vir e editar minha entidade de domínio. Este foi um exemplo muito simples, mas na RL a validação pode ser mais complexa

Então, seguindo a filosofia de Udi Dahan,tornando os papéis explícitos, e a recomendação de Eric Evans no livro azul, a próxima tentativa foi implementar o padrão de especificação, algo como isto

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

Mas então eu percebo que para seguir essa abordagem eu tive que mutar minhas entidades primeiro para passarvalor sendo valdiatedNesse caso, o e-mail, mas alterá-los, acionaria meus eventos de domínio, o que eu não gostaria que acontecesse até que o novo e-mail seja válido

Então, depois de considerar essas abordagens, eu saí com essa, já que vou implementar uma arquitetura CQRS:

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

Bem, essa é a ideia principal, a entidade é passada para o validador no caso de precisarmos de algum valor da entidade para executar a validação, o comando contém os dados provenientes do usuário e desde que os validadores são consideradosinjetável objetos eles poderiam ter dependências externas injetadas se a validação exigir isso.

Agora o dilema, Estou feliz com um design como este porque minha validação é encapsulada em objetos individuais que traz muitas vantagens: teste de unidade fácil, fácil de manter, invariantes de domínio são explicitamente expressos usando a linguagem ubíqua, fácil de estender, lógica de validação é centralizada e validadores podem ser usados ​​juntos para impor regras de domínio complexas. E mesmo quando eu sei que estou colocando a validação de minhas entidades fora deles (você poderia argumentar um código de cheiro - domínio anêmico), mas eu acho que o trade-off é aceitável

Mas há uma coisa que não descobri como implementá-lo de maneira limpa.Como devo usar esses componentes ...

Como eles serão injetados, eles não se encaixam naturalmente em minhas entidades de domínio. Basicamente, vejo duas opções:

Passar os validadores para cada método da minha entidade

Validar meus objetos externamente (do manipulador de comandos)

Eu não estou feliz com a opção 1, então eu explicaria como eu faria com a opção 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();
      }
   }
}

Bem, é isso. Você pode me dar sua opinião sobre isso ou compartilhar suas experiências com a validação de entidades do domínio

EDITAR

Eu acho que não está claro na minha pergunta, mas o problema real é: Escondendo as regras de domínio tem sérias implicações na manutenção futura do aplicativo, e também as regras de domínio mudam frequentemente durante o ciclo de vida do aplicativo. Portanto, implementá-los com isso em mente nos permitiria ampliá-los facilmente. Agora, imagine que, no futuro, um mecanismo de regras seja implementado, se as regras forem encapsuladas fora das entidades de domínio, essa alteração será mais fácil de implementar

Estou ciente de que colocar a validação fora de minhas entidades quebra o encapsulamento como @jgauffin mencionado em sua resposta, mas acho que os benefícios de colocar a validação em objetos individuais são muito mais substanciais do que apenas manter o encapsulamento de uma entidade. Agora acho que o encapsulamento faz mais sentido em uma arquitetura tradicional de n camadas porque as entidades foram usadas em vários lugares da camada de domínio, mas em uma arquitetura CQRS, quando um comando chega, haverá um manipulador de comando acessando uma raiz agregada e executar operações contra o agregado raiz apenas criando uma janela perfeita para colocar a validação.

Eu gostaria de fazer uma pequena comparação entre as vantagens de colocar a validação dentro de uma entidade versus colocá-la em objetos individuais

Validação em objetos individuais

Pró. Fácil de escreverPró. Fácil de testarPró. É explicitamente expressoPró. Torna-se parte do design do Domínio, expresso com a linguagem ubíqua atualPró. Como agora faz parte do design, ele pode ser modelado usando diagramas UMLPró. Extremamente fácil de manterPró. Torna minhas entidades e a lógica de validação fracamente acopladasPró. Fácil de estenderPró. Seguindo o SRPPró. Seguindo o princípio abrir / fecharPró. Não quebrando a lei de Demeter (mmm)?Pró. Eu sou centralizadoPró. Poderia ser reutilizávelPró. Se necessário, as dependências externas podem ser facilmente injetadasPró. Se estiver usando um modelo de plug-in, novos validadores podem ser adicionados apenas descartando os novos assemblies sem a necessidade de recompilar todo o aplicativoPró. Implementar um mecanismo de regras seria mais fácilVigarista. Quebrando o encapsulamentoVigarista. Se o encapsulamento for obrigatório, teríamos que passar os validadores individuais para o método de entidade (agregado)

Validação encapsulada dentro da entidade

Pró. Encapsulado?Pró. Reutilizável?

Eu adoraria ler seus pensamentos sobre isso

questionAnswers(11)

yourAnswerToTheQuestion