Domain Validation in a CQRS architecture

Peligro ... Peligro Dr. Smith ... Poste filosófico por delante

El propósito de esta publicación es determinar si colocar la lógica de validación fuera de las entidades de mi dominio (en realidad, la raíz agregada) me otorga más flexibilidad o escódigo kamikaze

Básicamente, quiero saber si hay una mejor manera de validar mis entidades de dominio. Así es como planeo hacerlo, pero me gustaría su opinión.

El primer enfoque que consideré fue:

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

En realidad no me gusta esta validación porque incluso cuando estoy encapsulando la lógica de validación en la entidad correcta, esto está violando el principio de Abrir / Cerrar (Abierto por extensión pero Cerrar por modificación) y he encontrado que violando este principio, el mantenimiento del código se convierte en Un verdadero dolor cuando la aplicación crece en complejidad. ¿Por qué? Debido a que las reglas de dominio cambian más a menudo de lo que nos gustaría admitir, y si las reglas sonoculto e incrustado en una entidad como esta, son difíciles de probar, de leer, de mantener, pero la razón real por la que no me gusta este enfoque es: si las reglas de validación cambian, tengo que venir y editar mi entidad de dominio. Este ha sido un ejemplo muy simple pero en RL la validación podría ser más compleja

Así que siguiendo la filosofía de Udi Dahan,haciendo roles explícitos, y la recomendación de Eric Evans en el libro azul, el siguiente intento fue implementar el patrón de especificación, algo como esto.

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

Pero luego me doy cuenta de que para seguir este enfoque, primero tenía que mutar mis entidades para pasar elvalor siendo validado, en este caso, el correo electrónico, pero si se muta, se activarán los eventos de mi dominio, lo que no me gustaría que sucediera hasta que el nuevo correo electrónico sea válido.

Entonces, después de considerar estos enfoques, salí con este, ya que voy a implementar una arquitectura CQRS:

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

Bueno, esa es la idea principal: la entidad se pasa al validador en caso de que necesitemos algún valor de la entidad para realizar la validación, el comando contiene los datos provenientes del usuario y, dado que los validadores se consideraninyectable Los objetos pueden tener dependencias externas inyectadas si la validación lo requiere.

Ahora el dilemaEstoy contento con un diseño como este porque mi validación está encapsulada en objetos individuales, lo que brinda muchas ventajas: prueba unitaria fácil, fácil de mantener, los invariantes de dominio se expresan explícitamente utilizando el lenguaje ubicuo, fácil de extender, la lógica de validación es centralizada y los validadores se pueden utilizar juntos para hacer cumplir reglas de dominio complejas. E incluso cuando sé que estoy colocando la validación de mis entidades fuera de ellas (podría argumentar un olor de código - dominio anémico) pero creo que la compensación es aceptable

Pero hay una cosa que no he descubierto cómo implementarla de una manera limpia.¿Cómo debo usar estos componentes ...

Ya que serán inyectados, no encajarán naturalmente dentro de las entidades de mi dominio, por lo que básicamente veo dos opciones:

Pasar los validadores a cada método de mi entidad.

Validar mis objetos externamente (desde el controlador de comandos)

No estoy contento con la opción 1, así que explicaría cómo lo haría con la opción 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();
      }
   }
}

Pues esto es todo. ¿Me puede dar su opinión sobre esto o compartir sus experiencias con la validación de entidades de dominio?

EDITAR

Creo que no queda claro en mi pregunta, pero el problema real es: ocultar las reglas del dominio tiene serias implicaciones en la mantenibilidad futura de la aplicación, y también las reglas del dominio cambian a menudo durante el ciclo de vida de la aplicación. Por lo tanto, implementarlos con esto en mente nos permitiría extenderlos fácilmente. Ahora imagine que en el futuro se implementará un motor de reglas, si las reglas se encapsulan fuera de las entidades del dominio, este cambio sería más fácil de implementar

Soy consciente de que colocar la validación fuera de mis entidades rompe la encapsulación como @jgauffin mencionó en su respuesta, pero creo que los beneficios de colocar la validación en objetos individuales son mucho más sustanciales que simplemente mantener la encapsulación de una entidad. Ahora creo que la encapsulación tiene más sentido en una arquitectura tradicional de n niveles porque las entidades se usaron en varios lugares de la capa de dominio, pero en una arquitectura CQRS, cuando llega un comando, habrá un controlador de comandos que accede a una raíz agregada y realizar operaciones contra la raíz agregada solo creando una ventana perfecta para colocar la validación.

Me gustaría hacer una pequeña comparación entre las ventajas de colocar la validación dentro de una entidad y colocarla en objetos individuales

Validación en objetos individuales.

Pro. Fácil de escribirPro. Fácil de probarPro. Se expresa expresamentePro. Se convierte en parte del diseño del dominio, expresado con el actual lenguaje ubicuo.Pro. Como ahora es parte del diseño, se puede modelar utilizando diagramas UML.Pro. Extremadamente fácil de mantenerPro. Hace que mis entidades y la lógica de validación se acoplen librementePro. Fácil de extenderPro. Siguiendo el SRPPro. Siguiendo el principio Open / ClosePro. ¿No rompiendo la ley de demeter (mmm)?Pro. Estoy centralizadoPro. Podría ser reutilizablePro. Si es necesario, las dependencias externas pueden ser fácilmente inyectadas.Pro. Si se usa un modelo de complemento, se pueden agregar nuevos validadores simplemente eliminando los nuevos ensamblajes sin la necesidad de volver a compilar toda la aplicaciónPro. Implementar un motor de reglas sería más fácil.Estafa. Rompiendo encapsulaciónEstafa. Si la encapsulación es obligatoria, tendríamos que pasar los validadores individuales al método de entidad (agregado)

Validación encapsulada dentro de la entidad.

Pro. Encapsulado?Pro. Reutilizable?

Me encantaría leer tus pensamientos sobre esto.

Respuestas a la pregunta(11)

Su respuesta a la pregunta