c# - Objetos comerciales, validación y excepciones
validation exception (18)
He estado leyendo algunas preguntas y respuestas sobre excepciones y su uso. Parece ser una fuerte opinión de que las excepciones deberían plantearse solo para casos de excepción no controlados. Entonces eso me lleva a preguntarme cómo funciona la validación con objetos comerciales.
Digamos que tengo un objeto comercial con getters / setters para las propiedades en el objeto. Digamos que necesito validar que el valor está entre 10 y 20. Esta es una regla comercial, por lo que pertenece a mi objeto comercial. Entonces eso parece implicar que el código de validación va en mi setter. Ahora tengo mi UI de datos enlazados a las propiedades del objeto de datos. El usuario ingresa 5, por lo que la regla debe fallar y el usuario no puede salir del cuadro de texto. . La interfaz de usuario está unida a la propiedad por lo que se llamará al instalador, se verificará la regla y se producirá un error. Si planteé una excepción de mi objeto de negocio para decir que la regla falló, la IU la detectaría. Pero eso parece ir en contra del uso preferido para excepciones. Dado que es un setter, realmente no vas a tener un ''resultado'' para el setter. Si establezco otro marcador en el objeto, eso implicaría que la UI debe verificar ese indicador después de cada interacción de UI.
Entonces, ¿cómo debería funcionar la validación?
Editar: probablemente he usado un ejemplo simplificado aquí. Algo similar al control de rango anterior podría manejarse fácilmente con la UI, pero ¿qué ocurre si la validación es más complicada, por ejemplo, el objeto comercial calcula un número basado en la entrada y si ese número calculado está fuera de rango debe ser rechazado. Esta es una lógica más complicada que no debería estar en la interfaz de usuario.
También existe la consideración de más datos ingresados en base a un campo ya ingresado. Por ejemplo, tengo que ingresar un artículo en la orden para obtener cierta información como stock disponible, costo actual, etc. El usuario puede requerir esta información para tomar decisiones sobre entradas posteriores (cuántas unidades ordenar) o puede ser requerida para para que se realice una mayor validación ¿Debería un usuario ingresar otros campos si el artículo no es válido? ¿Cuál sería el punto?
¿Has considerado plantear un evento en el colocador si los datos no son válidos? Eso evitaría el problema de lanzar una excepción y eliminaría la necesidad de verificar explícitamente el objeto para una bandera "inválida". Incluso podría pasar un argumento que indique qué campo falló la validación, para que sea más reutilizable.
El manejador del evento debe ser capaz de encargarse de volver a poner el foco en el control apropiado si es necesario, y podría contener cualquier código necesario para notificar al usuario del error. Además, simplemente puede negarse a conectar el controlador de eventos y tener la libertad de ignorar el error de validación si es necesario.
Como se menciona en el artículo de Paul Stovell, puede implementar la validación libre de errores en sus objetos comerciales mediante la implementación de la interfaz IDataErrorInfo. Al hacerlo, se permitirá la notificación de errores del usuario por el ErrorProvider de WinForm y el enlace de WPF con las reglas de validación . La lógica para validar las propiedades de los objetos se almacena en un método, en lugar de en cada uno de los captadores de propiedades, y no necesariamente tiene que recurrir a marcos como CSLA o Validation Application Block.
En lo que respecta a evitar que el usuario cambie el foco del cuadro de texto, en primer lugar, esta no suele ser la mejor práctica. Un usuario puede querer completar el formulario fuera de orden o, si una regla de validación depende de los resultados de múltiples controles, el usuario puede tener que completar un valor ficticio solo para salir de un control y establecer otro control. Dicho esto, esto se puede implementar estableciendo la propiedad AllowValidate
del Formulario en su valor predeterminado, EnableAllowFocusChange
y suscribiéndose al evento Control.Validating:
private void textBox1_Validating(object sender, CancelEventArgs e)
{
if (textBox1.Text != String.Empty)
{
errorProvider1.SetError(sender as Control, "Can not be empty");
e.Cancel = true;
}
else
{
errorProvider1.SetError(sender as Control, "");
}
}
El uso de reglas almacenadas en el objeto comercial para esta validación es un poco más complicado ya que se llama al evento de validación antes de que cambie el enfoque y se actualice el objeto comercial vinculado a datos.
Creo que depende de cuánto importe su modelo de negocio. Si quieres seguir el camino DDD, tu modelo es lo más importante. Por lo tanto, quiere que esté en un estado válido en todo momento.
En mi opinión, la mayoría de las personas intentan hacer demasiado (comunicarse con las vistas, persistir en la base de datos, etc.) con los objetos del dominio, pero a veces necesita más capas y una mejor separación de preocupaciones, es decir, uno o más Modelos de Vista. Luego puede aplicar la validación sin excepciones en su Modelo de Vista (la validación puede ser diferente para diferentes contextos, por ejemplo, servicios web / sitio web / etc.) y mantener validaciones de excepciones dentro de su modelo comercial (para evitar que el modelo se corrompa). Necesitará una (o más) capa de Servicio de Aplicación para mapear su Modelo de Vista con su Modelo de Negocio. Los objetos comerciales no deben estar contaminados con atributos de validación a menudo relacionados con marcos específicos, por ejemplo, NHibernate Validator.
Definitivamente recomendaría la validación tanto del cliente como del servidor (o la validación en las distintas capas). Esto es especialmente importante cuando se comunica a través de niveles o procesos físicos, ya que el costo de lanzar excepciones se vuelve cada vez más costoso. Además, cuanto más abajo de la cadena se espera la validación, más tiempo se desperdicia.
Como usar Excepciones o no para la validación de datos. Creo que está bien usar excepción en proceso (aunque aún no es preferible), pero fuera del proceso, llame a un método para validar el objeto comercial (por ejemplo, antes de guardar) y haga que el método devuelva el éxito de la operación junto con cualquier error de validación. Los errores no son excepcionales.
Microsoft lanza excepciones de objetos comerciales cuando falla la validación. Al menos, así es como funciona el Bloque de la Aplicación de Validación de la Biblioteca Empresarial.
using Microsoft.Practices.EnterpriseLibrary.Validation;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
public class Customer
{
[StringLengthValidator(0, 20)]
public string CustomerName;
public Customer(string customerName)
{
this.CustomerName = customerName;
}
}
Depende del tipo de validación que realizará y dónde. Creo que cada capa de la aplicación puede protegerse fácilmente contra datos erróneos y es demasiado fácil de hacer para que no valga la pena.
Considere una aplicación de varios niveles y los requisitos / instalaciones de validación de cada capa. La capa intermedia, Objeto, es la que parece estar en debate aquí.
Base de datos
se protege de un estado no válido con restricciones de columna e integridad referencial, lo que hará que el código de la base de datos de la aplicación genere excepcionesObjeto
?ASP.NET/Windows Forms
protege el estado del formulario (no el objeto) usando rutinas y / o controles de validador sin usar excepciones (winforms no incluye validadores, pero hay una excelente serie en msdn que describe cómo implementarlos )
Digamos que tiene una tabla con una lista de habitaciones de hotel, y cada fila tiene una columna para el número de camas llamadas ''camas''. El tipo de datos más sensato para esa columna es un pequeño entero sin signo *. También tiene un objeto ole simple con una propiedad Int16 * llamada ''Camas''. El problema es que puede pegar -4555 en un Int16, pero cuando vaya a conservar los datos en una base de datos obtendrá una excepción. Lo cual está bien, no se debe permitir que mi base de datos diga que una habitación de hotel tiene menos de cero camas, porque una habitación de hotel no puede tener menos de cero camas.
* Si su base de datos puede representarlo, pero supongamos que puede
* Sé que puedes usar un ushort en C #, pero a los fines de este ejemplo, supongamos que no puedes
Existe cierta confusión sobre si los objetos deben representar su entidad comercial o si deben representar el estado de su formulario. Ciertamente en ASP.NET y Windows Forms, el formulario es perfectamente capaz de manejar y validar su propio estado. Si tiene un cuadro de texto en un formulario ASP.NET que se va a usar para poblar ese mismo campo Int16, probablemente haya puesto un control RangeValidator en su página que prueba la entrada antes de que se le asigne a su objeto. Le impide ingresar un valor inferior a cero, y probablemente le impida ingresar un valor superior a, por ejemplo, 30, que con suerte sería suficiente para atender al peor albergue infestado de pulgas que pueda imaginar. En la devolución de datos, probablemente esté revisando la propiedad IsValid de la página antes de construir su objeto, evitando así que su objeto represente siempre menos de cero camas y evitando que se llame a su instalador con un valor que no debería contener.
Pero su objeto todavía es capaz de representar menos de cero camas, y una vez más, si estaba usando el objeto en un escenario que no incluye las capas que tienen la validación integrada en ellas (su formulario y su base de datos) no tiene suerte.
¿Por qué estarías alguna vez en este escenario? ¡Debe ser un conjunto de circunstancias bastante excepcional ! Por lo tanto, el usuario debe lanzar una excepción cuando recibe datos no válidos. Nunca debería ser arrojado, pero podría ser. Podría escribir un formulario de Windows para administrar el objeto para reemplazar el formulario de ASP.NET y olvidarse de validar el rango antes de rellenar el objeto. Podría estar utilizando el objeto en una tarea programada donde no haya interacción del usuario y que guarde en un área diferente, pero relacionada, de la base de datos en lugar de la tabla a la que se asigna el objeto. En este último escenario, su objeto puede ingresar a un estado donde no es válido, pero no lo sabrá hasta que los resultados de otras operaciones comiencen a verse afectados por el valor no válido. Si está buscando y lanzando excepciones, eso es.
Desea profundizar un poco en el notable trabajo de Paul Stovell con respecto a la validación de datos. Él resumió sus ideas al mismo tiempo en este artículo . Comparto su punto de vista sobre el asunto, que implementé en mis propias bibliotecas .
Aquí están, en palabras de Pablo, las desventajas de arrojar excepciones en los incubadores (en base a una muestra donde una propiedad Name
no debería estar vacía):
- Puede haber ocasiones en las que realmente necesite tener un nombre vacío. Por ejemplo, como el valor predeterminado para un formulario "Crear una cuenta" .
- Si confía en esto para validar cualquier dato antes de guardar, se perderá los casos en que los datos ya no son válidos. Con eso, quiero decir, si carga una cuenta de la base de datos con un nombre vacío y no la cambia, es posible que nunca sepa que no es válida.
- Si no está utilizando el enlace de datos, debe escribir una gran cantidad de código con
try/catch
bloquestry/catch
para mostrar estos errores al usuario. Es muy difícil tratar de mostrar errores en el formulario a medida que el usuario lo completa.- No me gusta arrojar excepciones por cosas no excepcionales. Un usuario que establece el nombre de una cuenta en "Supercalafragilisticexpialadocious" no es una excepción, es un error. Esto es, por supuesto, algo personal.
- Hace que sea muy difícil obtener una lista de todas las reglas que se han roto. Por ejemplo, en algunos sitios web, verá mensajes de validación como "Se debe ingresar el nombre. Se debe ingresar la dirección. Se debe ingresar el correo electrónico" . Para mostrar eso, vas a necesitar muchos bloques de
try/catch
.
Y aquí hay reglas básicas para una solución alternativa:
- No tiene nada de malo tener un objeto comercial no válido, siempre que no intente persistir.
- Todas y cada una de las reglas rotas deben poder recuperarse del objeto comercial, de modo que el enlace de datos, así como su propio código, puedan ver si hay errores y manejarlos adecuadamente.
En mi experiencia, las reglas de validación rara vez son universales en todas las pantallas / formularios / procesos en una aplicación. Escenarios como este son comunes: en la página de agregar, puede estar bien que un objeto Persona no tenga un apellido, pero en la página de edición debe tener un apellido. Siendo ese el caso, he llegado a creer que la validación debería ocurrir fuera de un objeto, o las reglas deberían ser inyectadas en el objeto para que las reglas puedan cambiar dado un contexto. Válido / Inválido debe ser un estado explícito del objeto después de la validación o uno que se puede derivar mediante la verificación de una colección de reglas fallidas. Una regla empresarial fallida no es una excepción en mi humilde opinión.
Es posible que desee mover la validación fuera de los getters y setters. Puede tener una función o propiedad llamada IsValid que ejecute todas las reglas de validación. t llenaría un diccionario o hashtable con todas las "Reglas rotas". Este diccionario estaría expuesto al mundo exterior y puede usarlo para completar sus mensajes de error.
Este es el enfoque que se toma en CSLA.Net.
Lanzar una excepción en tu caso está bien. Puede considerar el caso como una verdadera excepción porque algo intenta establecer un entero en una cadena (por ejemplo). La falta de conocimiento de las vistas comerciales de sus vistas significa que deben considerar este caso como excepcional y devolverlo a la vista.
Ya sea que valide o no sus valores de entrada antes de enviarlos a la capa de negocios depende de usted, creo que siempre que siga el mismo estándar en toda su aplicación, entonces obtendrá un código limpio y legible.
Puede utilizar el marco de primavera como se especifica anteriormente, solo tenga cuidado ya que gran parte del documento vinculado indica código de escritura que no está fuertemente tipado, es decir, puede obtener errores en tiempo de ejecución que no pudo recuperar en tiempo de compilación. Esto es algo que trato de evitar tanto como sea posible.
La forma en que lo hacemos aquí actualmente es que tomamos todos los valores de entrada de la pantalla, los vinculamos a un objeto de modelo de datos y lanzamos una excepción si un valor tiene un error.
Las excepciones no se deben lanzar como una parte normal de la validación. La validación invocada desde objetos comerciales es una última línea de defensa, y solo debería ocurrir si la UI no puede verificar algo. Como tales, se pueden tratar como cualquier otra excepción de tiempo de ejecución.
Tenga en cuenta que aquí hay una diferencia entre definir reglas de validación y aplicarlas. Es posible que desee definir (es decir, codificar o anotar) las reglas de su negocio en la capa de lógica de su empresa, pero invocarlas desde la interfaz de usuario para que puedan ser manejadas de manera adecuada a esa interfaz de usuario en particular. La forma de manejo variará para diferentes UI, por ejemplo, aplicaciones web basadas en formularios vs aplicaciones web ajax. La validación de excepción en el set ofrece opciones muy limitadas para el manejo.
Muchas aplicaciones duplican sus reglas de validación, como en javascript, restricciones de objetos de dominio y restricciones de base de datos. Idealmente, esta información solo se definirá una vez, pero implementarla puede ser un desafío y requiere pensamiento lateral.
Quizás desee considerar el enfoque adoptado por el marco Spring . Si está usando Java (o .NET), puede usar Spring as-is, pero incluso si no lo está, aún podría usar ese patrón; solo tendrías que escribir tu propia implementación de eso.
Si la entrada va más allá de la regla de negocio implementada por el objeto comercial, diría que es un caso no manejado por el objeto busines. Por lo tanto, lanzaría una excepción. Aunque el colocador "manejará" un 5 en su ejemplo, el objeto comercial no lo hará.
Sin embargo, para combinaciones más complejas de entrada, se requiere un método de validación, o de lo contrario terminará con validaciones bastante complejas diseminadas por todos lados.
En mi opinión, tendrá que decidir qué camino tomar dependiendo de la complejidad de la entrada permitida / no permitida.
Siempre he sido un fan del enfoque de Rocky Lhotka en el marco de CSLA (como lo menciona Charles). En general, ya sea que lo oriente el colocador o llame a un método de Validación explícito, el objeto comercial mantiene internamente una colección de objetos BrokenRule. La IU simplemente necesita verificar un método IsValid en el objeto, que a su vez verifica el número de BrokenRules y lo maneja de forma adecuada. Alternativamente, podría hacer que el método Validate genere fácilmente un evento que la IU podría manejar (probablemente el enfoque más limpio). También puede usar la lista de BrokenRules para mostrar los mensajes de error al uso en forma de resumen o al lado del campo apropiado. Aunque el marco de CSLA está escrito en .NET, el enfoque general se puede utilizar en cualquier idioma.
No creo que arrojar una excepción sea la mejor idea en este caso. Definitivamente sigo la escuela de pensamiento que dice que las Excepciones deben ser para circunstancias excepcionales, que no es un error de validación simple. Criar un evento OnValidationFailed sería la opción más limpia, en mi opinión.
Por cierto, nunca me ha gustado la idea de no permitir que el usuario abandone un campo cuando se encuentra en un estado no válido. Hay tantas situaciones en las que es posible que deba abandonar el campo temporalmente (quizás para establecer otro campo primero) antes de volver atrás y corregir el campo no válido. Creo que es solo un inconveniente innecesario.
Suponiendo que tiene un código de validación y persistencia (es decir, guardar en la base de datos) por separado, yo haría lo siguiente:
La IU debe realizar la validación. No arrojes excepciones aquí. Puede alertar al usuario sobre errores y evitar que se guarde el registro.
Su código de guardado de base de datos debe arrojar excepciones de argumento no válidas para datos incorrectos. Tiene sentido hacerlo aquí, ya que no puede continuar con la escritura de la base de datos en este punto. Idealmente, esto nunca debería suceder, ya que la IU debería evitar que el usuario guarde, pero aún así lo necesita para garantizar la coherencia de la base de datos. Además, es posible que llame a este código desde algo que no sea la interfaz de usuario (por ejemplo, actualizaciones por lotes) donde no hay validación de datos de la interfaz de usuario.
Sus objetos comerciales deben arrojar excepciones para entradas malas, pero esas excepciones nunca deben arrojarse en el curso de una ejecución normal del programa. Sé que suena contradictorio, así que lo explicaré.
Cada método público debe validar sus entradas y lanzar "ArgumentException" s cuando son incorrectas. (Y los métodos privados deberían validar sus entradas con "Debug.Assert ()" para facilitar el desarrollo, pero esa es otra historia). Esta regla sobre la validación de entradas a métodos públicos (y propiedades, por supuesto) es verdadera para cada capa de la aplicación .
Los requisitos de la interfaz del software deben explicarse en la documentación de la interfaz, por supuesto, y el trabajo del código de llamada es asegurarse de que los argumentos sean correctos y las excepciones nunca se lanzarán, lo que significa que la UI necesita validar el entradas antes de entregarlas al objeto comercial.
Si bien las reglas dadas anteriormente casi nunca se deben romper, a veces la validación de objetos de negocios puede ser muy compleja y esa complejidad no debe ser impuesta en la IU. En ese caso, es bueno que la interfaz de BO permita cierto margen de maniobra en lo que acepta y luego proporcione un predicado de validación explícita (cadena de caracteres []) para verificar las propiedades y dar su opinión sobre lo que se debe cambiar. Pero observe en este caso que todavía hay requisitos de interfaz bien definidos y que no se deben lanzar excepciones (suponiendo que el código de llamada sigue las reglas).
Siguiendo este último sistema, casi nunca realizo la validación temprana en los emisores de propiedades, ya que ese blando complica el uso de las propiedades (pero en el caso dado en la pregunta, podría). (Por otro lado, por favor no me impida escabullirme de un campo solo porque tiene datos malos. ¡Me da miedo claustrofóbico cuando no puedo tabular alrededor de un formulario! Volveré y lo arreglaré en un minuto ¡Lo prometo! De acuerdo, me siento mejor ahora, lo siento.)
Tal vez debería considerar tener validación tanto del lado del cliente como del lado del servidor. Si algo pasa por alto la validación del lado del cliente, entonces puede lanzar una excepción si su objeto comercial se invalida.
Un enfoque que he usado era aplicar atributos personalizados a las propiedades del objeto comercial, que describían las reglas de validación. p.ej:
[MinValue(10), MaxValue(20)]
public int Value { get; set; }
Los atributos se pueden procesar y usar para crear automáticamente métodos de validación tanto del lado del cliente como del lado del servidor, para evitar el problema de duplicar la lógica comercial.
Tiendo a creer que los objetos comerciales deben arrojar excepciones cuando se pasan valores que violan sus reglas comerciales. Sin embargo, parece que la arquitectura de enlace de datos de winforms 2.0 asume lo opuesto, por lo que la mayoría de la gente está convencida de apoyar esta arquitectura.
Estoy de acuerdo con la última respuesta de shabbyrobe de que los objetos comerciales deberían ser útiles y funcionar correctamente en múltiples entornos y no solo en el entorno winforms, por ejemplo, el objeto comercial podría ser utilizado en un servicio web tipo SOA, una interfaz de línea de comando, asp .net, etc. El objeto debe comportarse correctamente y protegerse contra datos no válidos en todos estos casos.
Un aspecto que a menudo se pasa por alto es también lo que ocurre al gestionar las colaboraciones entre objetos en las relaciones 1-1, 1-n o nn, si también aceptan la adición de colaboradores no válidos y solo mantienen un indicador de estado no válido que debería verificarse o debería se niega activamente a agregar colaboraciones no válidas. Debo admitir que estoy muy influenciado por el enfoque de modelado de objetos simplificado (SOM) de Jill Nicola et al. Pero qué más es lógico
El siguiente paso es cómo trabajar con formularios de Windows. Estoy buscando crear un contenedor de interfaz de usuario para los objetos comerciales para estos escenarios.
Yo, en mi opinión, este es un ejemplo donde arrojar una excepción está bien. Es probable que su propiedad no tenga ningún contexto para corregir el problema, ya que tal excepción está en orden y, si es posible, el código de llamada debe manejar la situación.