Donde deberíamos poner validación para el modelo de dominio

38

Todavía estoy buscando las mejores prácticas para la validación del modelo de dominio. ¿Es bueno poner la validación en el constructor del modelo de dominio? mi ejemplo de validación del modelo de dominio de la siguiente manera:

public class Order
 {
    private readonly List<OrderLine> _lineItems;

    public virtual Customer Customer { get; private set; }
    public virtual DateTime OrderDate { get; private set; }
    public virtual decimal OrderTotal { get; private set; }

    public Order (Customer customer)
    {
        if (customer == null)
            throw new  ArgumentException("Customer name must be defined");

        Customer = customer;
        OrderDate = DateTime.Now;
        _lineItems = new List<LineItem>();
    }

    public void AddOderLine //....
    public IEnumerable<OrderLine> AddOderLine { get {return _lineItems;} }
}


public class OrderLine
{
    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }
    public virtual int Quantity { get; set; }
    public virtual decimal UnitPrice { get; set; }

    public OrderLine(Order order, int quantity, Product product)
    {
        if (order == null)
            throw new  ArgumentException("Order name must be defined");
        if (quantity <= 0)
            throw new  ArgumentException("Quantity must be greater than zero");
        if (product == null)
            throw new  ArgumentException("Product name must be defined");

        Order = order;
        Quantity = quantity;
        Product = product;
    }
}

Gracias por toda su sugerencia.

adisembiring
fuente

Respuestas:

47

Hay un interesante artículo de Martin Fowler sobre ese tema que destaca un aspecto que la mayoría de las personas (incluyéndome a mí) tienden a pasar por alto:

Pero una cosa que creo que constantemente hace tropezar a las personas es cuando piensan que la validez de objeto en un contexto independiente, como lo implica un método isValid.

Creo que es mucho más útil pensar en la validación como algo que está vinculado a un contexto, generalmente una acción que desea hacer. ¿Es este pedido válido para ser completado? ¿Es válido este cliente para registrarse en el hotel? Entonces, en lugar de tener métodos como isValid, tenga métodos como isValidForCheckIn.

De esto se deduce que el constructor no debe hacer la validación, excepto quizás alguna comprobación de cordura muy básica compartida por todos los contextos.

De nuevo del artículo:

En About Face, Alan Cooper abogó por que no debemos permitir que nuestras ideas de estados válidos eviten que un usuario ingrese (y guarde) información incompleta. Esto me recordó hace unos días cuando leí un borrador de un libro en el que Jimmy Nilsson está trabajando. Declaró un principio de que siempre debe poder guardar un objeto, incluso si tiene errores. Si bien no estoy convencido de que esto deba ser una regla absoluta, creo que las personas tienden a evitar ahorrar más de lo que deberían. Pensar en el contexto para la validación puede ayudar a prevenir eso.

Michael Borgwardt
fuente
Gracias a Dios que alguien dijo esto. Los formularios que tienen el 90% de los datos pero que no guardan nada son injustos para los usuarios, que a menudo representan el otro 10% solo para no perder datos, por lo que todo lo que se ha validado es forzar al sistema a perder de vista el 10% estaba formado por. Problemas similares pueden ocurrir en el back-end, digamos una importación de datos. He descubierto que generalmente es mejor intentar trabajar correctamente con datos no válidos que tratar de evitar que suceda.
psr
2
@psr ¿Necesitas incluso lógica de back-end si tus datos no son persistentes? Puede dejar toda la manipulación en el lado del cliente si sus datos no tienen sentido en su modelo de negocio. También sería un desperdicio de recursos enviar mensajes de ida y vuelta (cliente - servidor) si los datos no tienen sentido. Así que volvemos a la idea de "¡nunca permitir que los objetos de dominio entren en estado no válido!" .
Geo C.
2
Me pregunto por qué tantos votos por una respuesta tan ambigua. Cuando se usa DDD, a veces hay algunas reglas que simplemente verifican si algunos datos son INT o están en un rango. Por ejemplo, cuando permite que el usuario de su aplicación elija algunas restricciones en sus productos (cuántas veces puede alguien obtener una vista previa de mi producto y en qué intervalo de días de un mes). Aquí ambas restricciones deben ser int y una de ellas debe estar en un rango de 0-31. Esto parece una validación del formato de datos que en un entorno no DDD cabría en un servicio o controlador. Pero en DDD estoy del lado de mantener la validación en el dominio (90%).
Geo C.
2
Hacer que las capas superiores sepan demasiado sobre el dominio para mantenerlo en un estado válido huele a mal diseño. El dominio debe ser el que garantice que su estado sea válido. Moverse demasiado sobre los hombros de las capas superiores puede hacer que su dominio sea anémico y podría deslizar algunas restricciones importantes que podrían dañar su negocio. De lo que me doy cuenta ahora, una generalización adecuada sería mantener su validación lo más cerca posible de su persistencia, o tan cerca de su código de manipulación de datos (cuando se manipula para alcanzar un estado final).
Geo C.
PD: No mezclo autorización (se le permite hacer algo), autenticación (el mensaje vino de la ubicación correcta o fue enviado por el cliente correcto, ambos identificados por clave de API / token / nombre de usuario o cualquier otra cosa) con validación de formato o reglas de negocios. Cuando digo 90%, me refiero a las reglas comerciales que la mayoría de ellas también incluyen la validación de formato. Por supuesto, la validación del formato puede estar en capas superiores, pero la mayoría de ellas estarán en el dominio (incluso el formato de dirección de correo electrónico que se validará en el objeto de valor EmailAddress).
Geo C.
6

A pesar de que esta pregunta es un poco rancia, me gustaría agregar algo que valga la pena:

Me gustaría estar de acuerdo con @MichaelBorgwardt y extender al mencionar la capacidad de prueba. En "Trabajar eficazmente con el código heredado", Michael Feathers habla mucho sobre los obstáculos para las pruebas y uno de esos obstáculos es los objetos "difíciles de construir". La construcción de un objeto no válido debería ser posible y, como sugiere Fowler, las comprobaciones de validez dependientes del contexto deberían poder identificar esas condiciones. Si no puede descubrir cómo construir un objeto en un arnés de prueba, tendrá problemas para evaluar su clase.

En cuanto a la validez, me gusta pensar en los sistemas de control. Los sistemas de control funcionan analizando constantemente el estado de una salida y aplicando acciones correctivas a medida que la salida se desvía del punto de ajuste, esto se denomina control de bucle cerrado. El control de circuito cerrado espera intrínsecamente las desviaciones y actúa para corregirlas, y así es como funciona el mundo real, razón por la cual todos los sistemas de control reales suelen utilizar controladores de circuito cerrado.

Creo que usar una validación dependiente del contexto y objetos fáciles de construir hará que su sistema sea más fácil de trabajar en el futuro.

Paul
fuente
1
Muchas veces los objetos solo parecen difíciles de construir. Por ejemplo, en este caso, puede omitir el constructor público creando una clase Wrapper que herede de la clase que se está probando y le permita crear una instancia del objeto base en un estado no válido. Aquí es donde entra en juego el uso de los modificadores de acceso correctos en las clases y los constructores y puede ser realmente perjudicial para las pruebas si se usa incorrectamente. Además, evitar clases y métodos "sellados", excepto cuando sea apropiado, ayudará mucho a que un código sea más fácil de probar.
P. Roe
4

Como estoy seguro de que ya sabes ...

En la programación orientada a objetos, un constructor (a veces acortado a ctor) en una clase es un tipo especial de subrutina llamada en la creación de un objeto. Prepara el nuevo objeto para su uso, a menudo acepta parámetros que el constructor usa para establecer las variables miembro necesarias cuando se crea el objeto por primera vez. Se llama constructor porque construye los valores de los miembros de datos de la clase.

Verificar la validez de los datos pasados ​​como parámetros de c'tor es definitivamente válido en el constructor; de lo contrario, posiblemente esté permitiendo la construcción de un objeto no válido.

Sin embargo (y esta es solo mi opinión, no puedo encontrar ningún buen documento en este momento): si la validación de datos requiere operaciones complejas (como operaciones asíncronas, tal vez validación basada en el servidor si se desarrolla una aplicación de escritorio), entonces es mejor coloca una función de inicialización o validación explícita de algún tipo y los miembros establecen los valores predeterminados (como null) en el c'tor.


Además, solo como nota al margen como lo incluyó en su ejemplo de código ...

A menos que realice una validación adicional (u otra funcionalidad) AddOrderLine, lo más probable es que lo exponga List<LineItem>como una propiedad en lugar de Orderactuar como una fachada .

Demian Brecht
fuente
¿Por qué exponer el contenedor? ¿Qué le importa a las capas superiores qué es el contenedor? Es perfectamente razonable tener un AddLineItemmétodo. De hecho, para DDD, esto es preferido. Si List<LineItem>se cambia a un objeto de colección personalizado, la propiedad expuesta y todo lo que dependía de una List<LineItem>propiedad están sujetos a cambio, error y excepción.
IAbstract
4

La validación debe realizarse lo antes posible.

La validación en cualquier contexto, ya sea el modelo de dominio o cualquier otra forma de escribir software, debe servir para lo que desea validar y en qué nivel se encuentra en este momento.

Según su pregunta, supongo que la respuesta sería dividir la validación.

  1. La validación de la propiedad verifica si el valor de esa propiedad es correcto, por ejemplo, cuando se tiene un rango entre 1-10.

  2. La validación de objetos garantiza que todas las propiedades en el objeto son válidas en conjunto entre sí. Por ejemplo, BeginDate es anterior a EndDate. Suponga que lee un valor del almacén de datos y que BeginDate y EndDate se inicializan en DateTime.Min de forma predeterminada. Al configurar BeginDate, no hay ninguna razón para aplicar la regla "debe ser anterior a EndDate", ya que esto no se aplica AÚN. Esta regla debe verificarse DESPUÉS de que se hayan establecido todas las propiedades. Esto se puede llamar a nivel raíz agregado

  3. La validación también debe realizarse en la entidad agregada (o raíz agregada). Un objeto de pedido puede contener datos válidos y también lo son sus líneas de pedido. Pero luego, una regla comercial establece que ningún pedido puede superar los $ 1,000. ¿Cómo haría cumplir esta regla en algunos casos? no puede simplemente agregar una propiedad "no validar la cantidad" ya que esto conduciría a abuso (tarde o temprano, tal vez incluso usted, solo para eliminar esta "solicitud desagradable").

  4. a continuación hay validación en la capa de presentación. ¿Realmente vas a enviar el objeto a través de la red, sabiendo que fallará? ¿O le ahorrará al usuario este perdón y le informará tan pronto como ingrese un valor no válido? Por ejemplo, la mayoría de las veces su entorno DEV será más lento que la producción. ¿Le gustaría esperar 30 segundos antes de que le informen de "olvidó este campo OTRA VEZ durante OTRA prueba", especialmente cuando hay un error de producción que se debe corregir con su jefe respirando por el cuello?

  5. Se supone que la validación en el nivel de persistencia es lo más cercana posible a la validación del valor de la propiedad. Esto ayudará a evitar excepciones al leer errores "nulos" o de "valor no válido" al usar mapeadores de cualquier tipo o lectores de datos antiguos. El uso de procedimientos almacenados resuelve este problema, pero requiere escribir la misma lógica de valuación OTRA VEZ y ejecutarla OTRA VEZ. Y los procedimientos almacenados son el dominio de administración de la base de datos, por lo que no intente hacer SU trabajo también (o peor aún, molestarlo con esta "elección minuciosa por la que no se le paga").

para decirlo con algunas palabras famosas "depende", pero al menos ahora sabes POR QUÉ depende.

Desearía poder colocar todo esto en un solo lugar, pero desafortunadamente, esto no se puede hacer. Hacer esto colocaría una dependencia en un "objeto de Dios" que contiene TODA la validación para TODAS las capas. No quieres ir por ese camino oscuro.

Por esta razón, solo lanzo excepciones de validación a un nivel de propiedad. Todos los demás niveles utilizo ValidationResult con un método IsValid para recopilar todas las "reglas rotas" y pasarlas al usuario en una única AggregateException.

Al propagar la pila de llamadas, las vuelvo a reunir en AggregateExceptions hasta llegar a la capa de presentación. La capa de servicio puede lanzar esta excepción directamente al cliente en caso de WCF como una FaultException.

Esto me permite tomar la excepción y dividirla para mostrar errores individuales en cada control de entrada o aplanarla y mostrarla en una sola lista. La decisión es tuya.

Es por eso que también mencioné la validación de la presentación, para cortocircuitarlos tanto como sea posible.

En caso de que se pregunte por qué también tengo la validación a nivel de agregación (o nivel de servicio si lo desea), es porque no tengo una bola de cristal que me diga quién usará mis servicios en el futuro. Tendrá suficientes problemas para encontrar sus propios errores para evitar que otros cometan los suyos :) ingresando datos no válidos, por ejemplo, administra la aplicación A, pero la aplicación B alimenta algunos datos utilizando su servicio. ¿Adivina a quién preguntan primero cuando hay un error? El administrador de la aplicación B felizmente informará al usuario "no hay ningún error en mi extremo, solo ingreso los datos".

Wesley Kenis
fuente