¿Cómo debo manejar la entrada de usuario no válida?


12

He estado pensando en este tema por un tiempo y me gustaría tener opiniones de otros desarrolladores.

Tiendo a tener un estilo de programación muy defensivo. Mi bloque o método típico se ve así:

T foo(par1, par2, par3, ...)
{
    // Check that all parameters are correct, return undefined (null)
    // or throw exception if this is not the case.

    // Compute and (possibly) return result.
}

Además, durante el cálculo, verifico todos los punteros antes de desreferenciarlos. Mi idea es que, si hay algún error y algún puntero NULL debería aparecer en alguna parte, mi programa debería manejarlo bien y simplemente negarse a continuar el cálculo. Por supuesto, puede notificar el problema con un mensaje de error en el registro o algún otro mecanismo.

Para decirlo de una manera más abstracta, mi enfoque es

if all input is OK --> compute result
else               --> do not compute result, notify problem

Otros desarrolladores, entre ellos algunos colegas míos, utilizan otra estrategia. Por ejemplo, no verifican los punteros. Asumen que un código debe recibir la entrada correcta y no debe ser responsable de lo que sucede si la entrada es incorrecta. Además, si una excepción de puntero NULL bloquea el programa, se encontrará un error más fácilmente durante las pruebas y tendrá más posibilidades de ser reparado.

Mi respuesta a esto es normalmente: ¿pero qué pasa si el error no se encuentra durante las pruebas y aparece cuando el cliente ya está utilizando el producto? ¿Cuál es la forma preferida para que el error se manifieste? ¿Debería ser un programa que no realiza una determinada acción, pero que aún puede continuar funcionando, o un programa que se bloquea y necesita reiniciarse?

Resumiendo

¿Cuál de los dos enfoques para manejar una entrada incorrecta recomendaría?

Inconsistent input --> no action + notification

o

Inconsistent input --> undefined behaviour or crash

Editar

Gracias por las respuestas y sugerencias. Soy fanático del diseño por contrato también. Pero incluso si confío en la persona que ha escrito el código llamando a mis métodos (tal vez soy yo mismo), todavía puede haber errores, lo que lleva a una entrada incorrecta. Por lo tanto, mi enfoque es nunca suponer que un método pasa la entrada correcta.

Además, usaría un mecanismo para detectar el problema y notificarlo. En un sistema de desarrollo, por ejemplo, abriría un cuadro de diálogo para notificar al usuario. En un sistema de producción, solo escribiría alguna información en el registro. No creo que las comprobaciones adicionales puedan generar problemas de rendimiento. No estoy seguro de si las afirmaciones son suficientes, si se desactivan en un sistema de producción: tal vez ocurra alguna situación en la producción que no se haya producido durante las pruebas.

De todos modos, me sorprendió mucho que muchas personas sigan el enfoque opuesto: dejan que la aplicación se bloquee "a propósito" porque sostienen que esto hará que sea más fácil encontrar errores durante las pruebas.


Siempre codifique defensivamente. Finalmente, por razones de rendimiento, puede poner un interruptor para deshabilitar algunas pruebas en el modo de lanzamiento.
deadalnix

Hoy he solucionado un error relacionado con una comprobación de puntero NULL faltante. Se creó algún objeto durante el cierre de sesión de la aplicación y el constructor utilizó un captador para acceder a otro objeto que ya no estaba allí. El objeto no estaba destinado a ser creado en ese punto. Se creó debido a otro error: algunos temporizadores no se detuvieron durante el cierre de sesión -> se envió una señal -> el destinatario intentó crear un objeto -> el constructor consultó y usó otro objeto -> puntero NULL -> bloqueo ) Realmente no me gustaría que una situación tan desagradable bloquea mi aplicación.
Giorgio

1
Regla de reparación: cuando deba fallar, falle ruidosamente y lo antes posible.
deadalnix

"Regla de reparación: cuando debe fallar, falle ruidosamente y tan pronto como sea posible": supongo que todos los BSOD de Windows son una aplicación de esta regla. :-)
Giorgio

Respuestas:


8

Lo has entendido bien. Se paranoico. No confíes en otro código, incluso si es tu propio código. Olvidas las cosas, haces cambios, el código evoluciona. No confíes en el código externo.

Se hizo un buen punto arriba: ¿qué pasa si las entradas no son válidas pero el programa no se bloquea? Luego obtienes basura en la base de datos y errores en la línea.

Cuando se me solicita un número (por ejemplo, precio en dólares o número de unidades), me gusta ingresar "1e9" y ver qué hace el código. Puede pasar.

Hace cuatro décadas, cuando obtuve mi licenciatura en Ciencias de la Computación de UCBerkeley, nos dijeron que un buen programa es un 50% de manejo de errores. Se paranoico.


Sí, en mi humilde opinión, esta es una de las pocas situaciones en las que ser paranoico es una característica y no un problema.
Giorgio

"¿Qué sucede si las entradas no son válidas pero el programa no se bloquea? Entonces se obtienen basura en la base de datos y errores en la línea": en lugar de bloquearse, el programa podría negarse a realizar la operación y devolver un resultado indefinido. Indefinido se propagará a través del cálculo y no se generará basura. Pero el programa no necesita bloquearse para lograr esto.
Giorgio

Sí, pero mi punto es que el programa debe DETECTAR la entrada no válida y hacer frente a ella. Si la entrada no está marcada, funcionará en el sistema y las cosas desagradables vendrán más tarde. ¡Incluso estrellarse es mejor que eso!
Andy Canfield

Estoy totalmente de acuerdo con usted: mi método o función típica comienza con una secuencia de comprobaciones para asegurarme de que los datos de entrada sean correctos.
Giorgio

Hoy volví a confirmar que la estrategia "comprobar todo, no confiar en nada" suele ser una buena idea. Un colega mío tuvo una excepción de puntero NULL debido a una falta de verificación. Resultó que en ese contexto era correcto tener un puntero NULL porque algunos datos no se habían cargado, y era correcto verificar el puntero y simplemente no hacer nada cuando es NULL. :-)
Giorgio

7

Ya tienes la idea correcta

¿Cuál de los dos enfoques para manejar una entrada incorrecta recomendaría?

Entrada inconsistente -> sin acción + notificación

o mejor

Entrada inconsistente -> acción manejada adecuadamente

Realmente no puede adoptar un enfoque de corte de cookies para la programación (sí podría), pero terminaría con un diseño formulado que hace las cosas por costumbre en lugar de por elección consciente.

Tempera el dogmatismo con el pragmatismo.

Steve McConnell lo dijo mejor

Steve McConnell prácticamente escribió el libro ( Código completo ) sobre programación defensiva y este fue uno de los métodos que aconsejó que siempre debe validar sus entradas.

No recuerdo si Steve mencionó esto, sin embargo, debe considerar hacerlo para métodos y funciones no privados , y solo para otros cuando lo considere necesario.


2
En lugar de público, sugeriría, todos los métodos no privados para cubrir defensivamente los idiomas que tienen un concepto protegido, compartido o sin restricción de acceso (todo es público, implícitamente).
JustinC

3

Aquí no hay una respuesta "correcta", particularmente sin especificar el idioma, el tipo de código y el tipo de producto en el que podría entrar el código. Considerar:

  • El lenguaje importa. En Objective-C, a menudo está bien enviar mensajes a nil; no pasa nada, pero el programa tampoco se bloquea. Java no tiene punteros explícitos, por lo que los punteros nulos no son una gran preocupación allí. En C, debes ser un poco más cuidadoso.

  • Ser paranoico es sospechar o desconfiar de forma injustificada e injustificada. Probablemente no sea mejor para el software que para las personas.

  • Su nivel de preocupación debe ser acorde con el nivel de riesgo en el código y la probable dificultad de identificar cualquier problema que aparezca. ¿Qué pasa en el peor de los casos? ¿El usuario reinicia el programa y continúa donde lo dejó? La empresa pierde millones de dólares?

  • No siempre se puede identificar una entrada incorrecta. Puede comparar religiosamente sus punteros a cero, pero eso solo atrapa uno de los 2 ^ 32 valores posibles, casi todos los cuales son malos.

  • Existen muchos mecanismos diferentes para tratar los errores. De nuevo, depende hasta cierto punto del idioma. Puede usar macros de aserción, declaraciones condicionales, pruebas unitarias, manejo de excepciones, diseño cuidadoso y otras técnicas. Ninguno de ellos es infalible, y ninguno es apropiado para cada situación.

Por lo tanto, se reduce principalmente a donde desea poner la responsabilidad. Si está escribiendo una biblioteca para que otros la usen, probablemente quiera ser tan cuidadoso como sea razonablemente posible con las entradas que reciba, y hacer todo lo posible para emitir errores útiles cuando sea posible. En sus propias funciones y métodos privados, puede usar afirmaciones para detectar errores tontos, pero de lo contrario responsabilizar a la persona que llama (que es usted) de no pasar basura.


+1 - Buena respuesta. Mi principal preocupación es que una entrada incorrecta puede causar un problema que aparece en un sistema de producción (cuando es demasiado tarde para hacer algo al respecto). Por supuesto, creo que tiene toda la razón al decir que depende del daño que dicho problema pueda causar al usuario.
Giorgio

El lenguaje juega un papel importante. En PHP, la mitad del código de su método termina verificando de qué tipo es la variable y tomando la acción apropiada. En Java, si el método acepta un int, no puede pasarle nada más, por lo que su método termina siendo más claro.
cap

1

Definitivamente debería haber una notificación, como una excepción lanzada. Sirve para avisar a otros codificadores que pueden estar haciendo mal uso del código que escribió (tratando de usarlo para algo que no estaba destinado a hacer) de que su entrada no es válida o genera errores. Esto es muy útil para rastrear errores, mientras que si simplemente devuelve nulo, su código continuará hasta que intenten usar el resultado y obtengan una excepción de un código diferente.

Si su código encuentra un error durante una llamada a otro código (tal vez una actualización fallida de la base de datos) que está más allá del alcance de ese código en particular, realmente no tiene control sobre él y su único recurso es lanzar una excepción que explique qué ya sabes (solo lo que te indica el código que has llamado). Si sabe que ciertas entradas conducirán inevitablemente a ese resultado, simplemente no puede molestarse en ejecutar su código y lanzar una excepción que indique qué entrada no es válida y por qué.

En una nota más relacionada con el usuario final, es mejor devolver algo descriptivo pero simple para que cualquiera pueda entenderlo. Si su cliente llama y dice "el programa se bloqueó, corríjalo", tiene mucho trabajo en sus manos para rastrear qué salió mal y por qué, y con la esperanza de poder reproducir el problema. El uso de un manejo adecuado de las excepciones no solo puede evitar un bloqueo, sino también proporcionar información valiosa. Una llamada de un cliente que dice "El programa me está dando un error. Dice 'XYZ no es una entrada válida para el método M, porque Z es demasiado grande", o algo así, incluso si no tienen idea de lo que significa, usted saber exactamente dónde mirar. Además, dependiendo de las prácticas comerciales de su empresa, es posible que ni siquiera sea usted el que solucione estos problemas, por lo que es mejor dejarles un buen mapa.

Entonces, la versión corta de mi respuesta es que su primera opción es la mejor.

Inconsistent input -> no action + notify caller

1

Luché con este mismo problema mientras asistía a una clase universitaria de programación. Me incliné hacia el lado paranoico y tiendo a verificar todo, pero me dijeron que se trataba de un comportamiento equivocado.

Nos enseñaron "Diseño por contrato". El énfasis es que las condiciones previas, las invariantes y las condiciones posteriores se especifiquen en los comentarios y documentos de diseño. Como la persona que implementa mi parte del código, debería confiar en el arquitecto de software y empoderarlos siguiendo las especificaciones que incluirían las condiciones previas (qué entradas deben poder manejar mis métodos y qué entradas no se me enviarán) . La comprobación excesiva en cada llamada a método produce hinchazón.

Las afirmaciones deben usarse durante las iteraciones de compilación para verificar la corrección del programa (validación de precondiciones, invariantes, condiciones posteriores). Las afirmaciones se convertirían en la compilación de producción.


0

El uso de "afirmaciones" es el camino a seguir para notificar a los demás desarrolladores que están haciendo mal, en los métodos de "privado" solamente por supuesto . Habilitar / deshabilitarlos es solo un indicador para agregar / eliminar en el momento de la compilación y, como tal, es fácil eliminar aserciones del código de producción. También hay una gran herramienta para saber si de alguna manera lo estás haciendo mal en tus propios métodos.

En cuanto a la verificación de los parámetros de entrada dentro de los métodos públicos / protegidos, prefiero trabajar a la defensiva y verificar los parámetros y lanzar InvalidArgumentException o similares. Por eso hay aquí para. También depende de si está escribiendo una API o no. Si es una API, y aún más si es de código cerrado, valide mejor todo para que los desarrolladores sepan con precisión qué salió mal. De lo contrario, si la fuente está disponible para otros desarrolladores, no es en blanco y negro. Solo sea consistente con sus elecciones.

Editar: solo para agregar que si mira, por ejemplo, en el Oracle JDK, verá que nunca verifican "nulo" y dejan que el código se bloquee. Dado que de todos modos va a lanzar una NullPointerException, ¿por qué molestarse en buscar nulo y lanzar una excepción explícita? Supongo que tiene sentido.


En Java obtienes una excepción de puntero nulo. En C ++, un puntero nulo bloquea la aplicación. Quizás haya otros ejemplos: división por cero, índice fuera de rango, etc.
Giorgio
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.