¿Debo abstraer el marco de validación de la capa de Dominio?

Estoy usando FluentValidation para validar mis operaciones de servicio. Mi código se ve así:

using FluentValidation; IUserService { void Add(User user); } UserService : IUserService { public void Add(User user) { new UserValidator().ValidateAndThrow(user); userRepository.Save(user); } } 

UserValidator implementa FluentValidation.AbstractValidator.

DDD dice que la capa de dominio debe ser independiente de la tecnología.

Lo que estoy haciendo es usar un marco de validación en lugar de excepciones personalizadas.

¿Es una mala idea poner un marco de validación en la capa de dominio?

¿Al igual que la abstracción del repository?

Bueno, veo algunos problemas con su diseño, incluso si IUserValidator su dominio del marco mediante la statement de una interfaz IUserValidator .

Al principio, parece que eso llevaría a la misma estrategia de abstracción que para el Repositorio y otras preocupaciones de infraestructura, pero hay una gran diferencia en mi opinión.

Al usar repository.save(...) , en realidad no le importa la implementación desde la perspectiva del dominio, porque cómo persistir no es un tema de dominio.

Sin embargo, la aplicación invariable es una cuestión de dominio y no debería tener que profundizar en los detalles de la infraestructura (el UserValidtor ahora se puede ver como tal) para ver en qué consisten y eso es básicamente lo que terminará haciendo si lo hace por ese camino. ya que las reglas se expressían en los términos del marco y vivirían fuera del dominio.

¿Por qué viviría afuera?

 domain -> IUserRepository infrastructure -> HibernateUserRepository domain -> IUserValidator infrastructure -> FluentUserValidator 

Entidades siempre válidas

Tal vez haya un problema más fundamental con su diseño y que ni siquiera haría esa pregunta si se adhiriera a esa escuela: entidades siempre válidas.

Desde ese punto de vista, la aplicación invariante es responsabilidad de la propia entidad de dominio y, por lo tanto, ni siquiera debería poder existir sin ser válida. Por lo tanto, las reglas invariables se expresan simplemente como contratos y se lanzan excepciones cuando se violan.

El razonamiento detrás de esto es que muchos errores provienen del hecho de que los objetos están en un estado que nunca debieron haber estado. Para exponer un ejemplo que he leído de Greg Young:

SendUserCreationEmailService que ahora tenemos un SendUserCreationEmailService que toma un UserProfile … ¿cómo podemos racionalizar en ese servicio que Name no sea null ? ¿Lo revisamos de nuevo? O más probablemente … simplemente no se molesta en verificar y “esperar lo mejor”, espera que alguien se tome la molestia de validarlo antes de enviárselo. Por supuesto, con TDD, una de las primeras pruebas que deberíamos escribir es que si envío un cliente con un nombre null debería generar un error. Pero una vez que comenzamos a escribir este tipo de pruebas una y otra vez nos damos cuenta … “esperen si nunca permitimos que el nombre se vuelva nulo, no tendríamos todas estas pruebas” – comenta Greg Young en http://jeffreypalermo.com / blog / the-fallacy-of-the-always-valid-entity /

Ahora no me malinterprete, obviamente no puede hacer cumplir todas las reglas de validación de esa manera, ya que algunas reglas son específicas para ciertas operaciones comerciales que prohíben ese enfoque (por ejemplo, guardar copias de borrador de una entidad), pero estas reglas no se deben ver de la misma manera que la aplicación invariante, que son reglas que se aplican en todos los escenarios (por ejemplo, un cliente debe tener un nombre).

Aplicando el principio siempre válido a tu código

Si ahora miramos su código e intentamos aplicar el enfoque siempre válido, vemos claramente que el objeto UserValidator no tiene su lugar.

 UserService : IUserService { public void Add(User user) { //We couldn't even make it that far with an invalid User new UserValidator().ValidateAndThrow(user); userRepository.Save(user); } } 

Por lo tanto, no hay lugar para FluentValidation en el dominio en este punto. Si aún no está convencido, pregúntese cómo integraría los objetos de valor. ¿Tendrá un UsernameValidator para validar un objeto de valor de nombre de Username cada vez que se instancia? Claramente, eso no tiene ningún sentido y el uso de objetos de valor sería bastante difícil de integrar con el enfoque no siempre válido.

¿Cómo informamos todos los errores cuando se lanzan las excepciones?

Eso es realmente algo con lo que luché y lo he estado preguntando por un tiempo (y todavía no estoy del todo convencido de lo que voy a decir).

Básicamente, lo que he llegado a entender es que no es tarea del dominio recostackr y devolver errores, eso es un problema de UI. Si los datos no válidos llegan hasta el dominio, simplemente te atacan.

Por lo tanto, los marcos como FluentValidation encontrarán su hogar natural en la interfaz de usuario y validarán modelos de vista en lugar de entidades de dominio.

Lo sé, parece difícil aceptar que habrá algún nivel de duplicación, pero esto se debe principalmente a que probablemente eres un desarrollador de stack completa como yo, que trata con la interfaz de usuario y el dominio cuando, de hecho, esos pueden y deben ser vistos. como proyectos completamente diferentes. Además, al igual que el modelo de vista y el modelo de dominio, la validación del modelo de vista y la validación del dominio puede ser similar, pero tiene un propósito diferente.

Además, si todavía te preocupa ser SECO, alguien me dijo una vez que la reutilización de código también es un “acoplamiento” y creo que ese hecho es particularmente importante aquí.

Hacer frente a la validación diferida en el dominio

No voy a volver a explicar los aquí, pero hay varios enfoques para hacer frente a validaciones diferidas en el dominio, como el patrón de Especificación y el enfoque de Validación Diferida descrito por Ward Cunningham en su lenguaje de patrones de Cheques. Si tiene el libro Implementing Domain-Driven Design de Vaughn Vernon, también puede leer de las páginas 208-215.

Siempre es una cuestión de compensaciones

La validación es un tema extremadamente difícil y la prueba es que, hasta el día de hoy, las personas todavía no están de acuerdo en cómo se debe hacer. Hay tantos factores, pero al final lo que desea es una solución práctica, sostenible y expresiva. No siempre se puede ser un purista y debe aceptar el hecho de que algunas reglas se romperán (por ejemplo, es posible que deba filtrar algunos detalles de persistencia discretos en una entidad para usar su ORM de elección).

Por lo tanto, si crees que puedes vivir con el hecho de que algunos detalles de FluentValidation llegan a tu dominio y que es más práctico así, bueno, realmente no puedo decir si hará más daño que bien en el largo plazo, pero no lo haría

La respuesta a su pregunta depende del tipo de validación que quiera poner en la clase de validador. La validación puede ser parte del modelo de dominio y en su caso lo ha implementado con FluentValidation y no veo ningún problema con eso. La clave del modelo de dominio: puede usar su modelo de dominio en todas partes, por ejemplo, si su proyecto contiene parte web, api, integración con otros subsistemas. Cada módulo hace referencia a su modelo de dominio y funciona igual para todos.

Si lo entendí correctamente, no veo ningún problema en hacer esto siempre que se abstraiga como una preocupación de infraestructura al igual que su repo abstrae la tecnología de persistencia.

Como ejemplo, he creado para mis proyectos un IObjectValidator que devuelve validadores por tipo de objeto, y una implementación estática del mismo, para que no esté acoplado a la tecnología en sí.

 public interface IObjectValidator { void Validate(T instance, params string[] ruleSet); Task ValidateAsync(T instance, params string[] ruleSet); } 

Y luego lo implementé con Validación fluida así:

 public class FluentValidationObjectValidator : IObjectValidator { private readonly IDependencyResolver dependencyResolver; public FluentValidationObjectValidator(IDependencyResolver dependencyResolver) { this.dependencyResolver = dependencyResolver; } public void Validate(T instance, params string[] ruleSet) { var validator = this.dependencyResolver .Resolve>(); var result = ruleSet.Length == 0 ? validator.Validate(instance) : validator.Validate(instance, ruleSet: ruleSet.Join()); if(!result.IsValid) throw new ValidationException(MapValidationFailures(result.Errors)); } public async Task ValidateAsync(T instance, params string[] ruleSet) { var validator = this.dependencyResolver .Resolve>(); var result = ruleSet.Length == 0 ? await validator.ValidateAsync(instance) : await validator.ValidateAsync(instance, ruleSet: ruleSet.Join()); if(!result.IsValid) throw new ValidationException(MapValidationFailures(result.Errors)); } private static List MapValidationFailures(IEnumerable failures) { return failures .Select(failure => new ValidationFailure( failure.PropertyName, failure.ErrorMessage, failure.AttemptedValue, failure.CustomState)) .ToList(); } } 

Tenga en cuenta que también he abstraído mi contenedor IOC con un IDependencyResolver para poder usar cualquier implementación que desee. (usando Autofac en este momento).

Así que aquí hay un código de bonificación para autofac;)

 public class FluentValidationModule : Module { protected override void Load(ContainerBuilder builder) { // registers type validators builder.RegisterGenerics(typeof(IValidator<>)); // registers the Object Validator and configures the Ambient Singleton container builder .Register(context => SystemValidator.SetFactory(() => new FluentValidationObjectValidator(context.Resolve()))) .As() .InstancePerLifetimeScope() .AutoActivate(); } } 

Al código podría faltarle algunas de mis ayudantes y extensiones, pero creo que sería más que suficiente para hacerlo funcionar.

Espero haber ayudado 🙂

EDITAR:

Debido a que algunos codificadores prefieren no usar el “service locator anti pattern”, aquí hay un ejemplo muy simple sobre cómo eliminarlo y seguir siendo felices 🙂

El código proporciona una propiedad del diccionario que debe llenarse con todos sus validadores por Tipo.

 public class SimpleFluentValidationObjectValidator : IObjectValidator { public SimpleFluentValidationObjectValidator() { this.Validators = new Dictionary(); } public Dictionary Validators { get; private set; } public void Validate(T instance, params string[] ruleSet) { var validator = this.Validators[typeof(T)]; if(ruleSet.Length > 0) // no ruleset option for this example throw new NotImplementedException(); var result = validator.Validate(instance); if(!result.IsValid) throw new ValidationException(MapValidationFailures(result.Errors)); } public Task ValidateAsync(T instance, params string[] ruleSet) { throw new NotImplementedException(); } private static List MapValidationFailures(IEnumerable failures) { return failures .Select(failure => new ValidationFailure( failure.PropertyName, failure.ErrorMessage, failure.AttemptedValue, failure.CustomState)) .ToList(); } }