c# - driven - equinox github
¿Debo abstraer el marco de validación de la capa de dominio? (3)
Al igual que la abstracción del repositorio?
Bueno, veo algunos problemas con su diseño incluso si
IUserValidator
su dominio del marco declarando una interfaz
IUserValidator
.
Al principio, parece que eso conducirí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.
Cuando se usa
repository.save(...)
, en realidad no te importa en absoluto la implementación desde la perspectiva del dominio, porque la forma de persistir no es una preocupación del dominio.
Sin embargo, la aplicación invariable es un problema 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 sigue ese camino ya que las reglas se expresarí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
Quizás haya un problema más fundamental con su diseño y que ni siquiera estaría haciendo esa pregunta si se adhiere a esa escuela de: entidades siempre válidas.
Desde ese punto de vista, la aplicación invariante es responsabilidad de la entidad de dominio en sí misma y, por lo tanto, ni siquiera debería ser capaz de 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 deberían haber estado. Para exponer un ejemplo que leí de Greg Young:
SendUserCreationEmailService
que ahora tenemos unSendUserCreationEmailService
que toma unUserProfile
... ¿cómo podemos racionalizar en ese servicio queName
no seanull
? ¿Lo revisamos nuevamente? O más probablemente ... simplemente no se molesta en comprobar y "espera lo mejor", espera que alguien se haya molestado en validarlo antes de enviárselo. Por supuesto, usando TDD, una de las primeras pruebas que deberíamos escribir es que si envío un cliente con un nombrenull
, debería generar un error. Pero una vez que comenzamos a escribir este tipo de pruebas una y otra vez, nos damos cuenta ... "espere 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 malinterpretes, obviamente no puedes hacer cumplir todas las reglas de validación de esa manera, ya que algunas reglas son específicas de ciertas operaciones comerciales que prohíben ese enfoque (por ejemplo, guardar borradores de una entidad), pero estas reglas no se deben ver de la misma manera que la aplicación invariable, 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 su 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 momento.
Si todavía no está convencido, pregúntese cómo integraría los objetos de valor.
¿Tendrá un
UsernameValidator
para validar un objeto de valor 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 excepciones?
Eso es realmente algo con lo que luché y lo he estado preguntando por un tiempo (y todavía no estoy completamente convencido de lo que voy a decir).
Básicamente, lo que he entendido es que no es el trabajo del dominio recopilar y devolver errores, eso es una preocupación de la interfaz de usuario. Si los datos no válidos llegan al dominio, simplemente te afecta.
Por lo tanto, los marcos como FluentValidation encontrarán su hogar natural en la interfaz de usuario y validarán los modelos de vista en lugar de las entidades de dominio.
Lo sé, parece difícil de aceptar que habrá algún nivel de duplicación, pero esto se debe principalmente a que probablemente eres un desarrollador de pila completa como yo que se ocupa de la interfaz de usuario y el dominio cuando de hecho esos pueden y deberían 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 pueden ser similares pero tienen un propósito diferente.
Además, si todavía le preocupa estar SECO, alguien me dijo una vez que la reutilización del código también es "acoplamiento" y creo que ese hecho es particularmente importante aquí.
Manejo de validación diferida en el dominio
No voy a volver a explicarlos aquí, pero hay varios enfoques para tratar las 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 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 a partir de hoy la gente todavía no está de acuerdo sobre cómo debería hacerse. Hay muchos factores, pero al final lo que desea es una solución práctica, mantenible y expresiva. No siempre puede ser un purista y debe aceptar el hecho de que se romperán algunas reglas (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 a largo plazo, pero yo no lo haría
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 el marco de validación en la capa de dominio?
La respuesta a su pregunta depende del tipo de validación que desee 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 elementos 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 hacerlo, siempre que se abstraiga como un problema de infraestructura al igual que su repositorio 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, de modo que no estoy acoplado a la tecnología en sí.
public interface IObjectValidator
{
void Validate<T>(T instance, params string[] ruleSet);
Task ValidateAsync<T>(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>(T instance, params string[] ruleSet)
{
var validator = this.dependencyResolver
.Resolve<IValidator<T>>();
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>(T instance, params string[] ruleSet)
{
var validator = this.dependencyResolver
.Resolve<IValidator<T>>();
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<ValidationFailure> MapValidationFailures(IEnumerable<FluentValidationResults.ValidationFailure> failures)
{
return failures
.Select(failure =>
new ValidationFailure(
failure.PropertyName,
failure.ErrorMessage,
failure.AttemptedValue,
failure.CustomState))
.ToList();
}
}
Tenga en cuenta que también abstraje mi contenedor IOC con un IDependencyResolver para poder usar la implementación que quiera. (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<IDependencyResolver>())))
.As<IObjectValidator>()
.InstancePerLifetimeScope()
.AutoActivate();
}
}
Al código podría faltarle algunos de mis ayudantes y extensiones, pero creo que sería más que suficiente para que pueda continuar.
Espero haberte ayudado :)
EDITAR:
Dado que algunos codificadores prefieren no usar el "patrón anti localizador de servicios", aquí hay un ejemplo muy simple sobre cómo eliminarlo y aún así estar contento :)
El código proporciona una propiedad de diccionario que debe llenarse con todos sus validadores por Tipo.
public class SimpleFluentValidationObjectValidator : IObjectValidator
{
public SimpleFluentValidationObjectValidator()
{
this.Validators = new Dictionary<Type, IValidator>();
}
public Dictionary<Type, IValidator> Validators { get; private set; }
public void Validate<T>(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>(T instance, params string[] ruleSet)
{
throw new NotImplementedException();
}
private static List<ValidationFailure> MapValidationFailures(IEnumerable<FluentValidationResults.ValidationFailure> failures)
{
return failures
.Select(failure =>
new ValidationFailure(
failure.PropertyName,
failure.ErrorMessage,
failure.AttemptedValue,
failure.CustomState))
.ToList();
}
}