¿Diseño por contrato usando aserciones o excepciones? [cerrado]


123

Cuando se programa por contrato, una función o método primero verifica si se cumplen sus condiciones previas, antes de comenzar a trabajar en sus responsabilidades, ¿verdad? Las dos formas más importantes que hacer estas comprobaciones son por asserty por exception.

  1. afirmar falla solo en modo de depuración. Para asegurarse de que es crucial (unidad) probar todas las condiciones previas del contrato por separado para ver si realmente fallan.
  2. la excepción falla en el modo de depuración y liberación. Esto tiene el beneficio de que el comportamiento de depuración probado es idéntico al comportamiento de liberación, pero incurre en una penalización de rendimiento en tiempo de ejecución.

¿Cuál crees que es preferible?

Ver pregunta relevante aquí


3
Todo el punto detrás del diseño por contrato es que no es necesario (y posiblemente no debería) verificar las condiciones previas en tiempo de ejecución. Verifica la entrada antes de pasarla al método con las condiciones previas, así es como respeta su finalización del contrato. Si la entrada no es válida o viola su final del contrato, el programa generalmente fallará de todos modos a través de su curso normal de acciones (que usted desea).
void.pointer

Buena pregunta, ¡pero creo que realmente debería cambiar la respuesta aceptada (como muestran los votos también)!
DaveFar

Para siempre, lo sé, pero ¿esta pregunta debería tener la etiqueta c ++? Estaba buscando esta respuesta, para usar en otro idioma (Delpih) y no puedo imaginar ningún lenguaje que presente excepciones y afirmaciones que no sigan las mismas reglas. (Todavía estoy aprendiendo las pautas de desbordamiento de pila.)
Eric G

Respuesta muy sucinta dada en esta respuesta : "En otras palabras, las excepciones abordan la solidez de su aplicación, mientras que las aserciones abordan su corrección".
Shmuel Levine

Respuestas:


39

Deshabilitar la afirmación en las versiones de lanzamiento es como decir "Nunca tendré ningún problema en una versión de lanzamiento", lo que a menudo no es el caso. Por lo tanto, afirmar no debe deshabilitarse en una versión de lanzamiento. Pero tampoco quiere que la versión de lanzamiento se bloquee cuando se producen errores, ¿verdad?

Así que usa excepciones y úsalas bien. Use una jerarquía de excepciones buena y sólida y asegúrese de atrapar y puede enganchar la excepción lanzando su depurador para atraparlo, y en el modo de liberación puede compensar el error en lugar de un bloqueo directo. Es el camino más seguro.


44
Las afirmaciones son útiles como mínimo en los casos en que la verificación de la corrección sería ineficiente o ineficiente para implementar correctamente.
Casebash el

89
El punto en las aserciones no es corregir errores, sino alertar al programador. Mantenerlos habilitados en las versiones de lanzamiento es inútil por esa razón: ¿Qué ganarías al tener un disparo de aserción? El desarrollador no podrá saltar y depurarlo. Las afirmaciones son una ayuda para la depuración, no son un reemplazo para las excepciones (y tampoco las excepciones son un reemplazo para las afirmaciones). Las excepciones alertan al programa sobre una condición de error. Afirmar alertas al desarrollador.
jalf

12
Pero se debe usar una aserción cuando los datos internos se han corrompido después de la reparación: si se desencadena una aserción, no puede hacer suposiciones sobre el estado del programa porque significa que algo está / mal /. Si una afirmación se ha disparado, no puede asumir que ningún dato es válido. Es por eso que una versión de lanzamiento debería afirmar, no para decirle al programador dónde está el problema, sino para que el programa pueda cerrarse y no arriesgar problemas mayores. El programa debería hacer todo lo posible para facilitar la recuperación más adelante, cuando los datos sean confiables.
coppro

55
@jalf, aunque no puede enganchar a su depurador en las versiones de lanzamiento, puede aprovechar el registro para que los desarrolladores vean la información relevante a su afirmación fallando. En este documento ( martinfowler.com/ieeeSoftware/failFast.pdf ), Jim Shore señala: "Recuerde, un error que ocurre en el sitio del cliente se realizó a través de su proceso de prueba. Probablemente tenga problemas para reproducirlo. Estos errores son lo más difícil de encontrar, y una afirmación bien explicada que explique el problema podría ahorrarle días de esfuerzo ".
StriplingWarrior

55
Personalmente prefiero afirmaciones para el diseño por enfoques contractuales. Las excepciones son defensivas y hacen que el argumento verifique dentro de la función. Además, las condiciones previas de dbc no dicen "No funcionaré si usa valores fuera del rango de trabajo", sino "No garantizaré proporcionar la respuesta correcta, pero aún puedo hacerlo". Las afirmaciones proporcionan al desarrollador comentarios de que están llamando a una función con una violación de la condición, pero no impidan que la usen si sienten que conocen mejor. La violación podría causar excepciones, pero veo eso como algo diferente.
Matt_JD

194

La regla general es que debe usar aserciones cuando intente detectar sus propios errores y excepciones cuando intente detectar los errores de otras personas. En otras palabras, debe usar excepciones para verificar las condiciones previas para las funciones API públicas y cada vez que obtenga datos externos a su sistema. Debe usar afirmaciones para las funciones o datos que son internos de su sistema.


¿Qué pasa con serializar / deserializar sentado en diferentes módulos / aplicaciones y eventualmente no sincronizarse? Quiero decir, por parte del lector, siempre es mi error si intento leer las cosas de manera incorrecta, por lo que tiendo a usar afirmaciones, pero por otro lado tengo datos externos, que eventualmente pueden cambiar de formato sin previo aviso.
Slava

Si los datos son externos, debe usar excepciones. En este caso particular, probablemente también debería detectar esas excepciones y manejarlas de una manera razonable, en lugar de simplemente dejar que su programa muera. Además, mi respuesta es una regla general, no una ley de la naturaleza. :) Así que debes considerar cada caso individualmente.
Dima

Si su función f (int * x) contiene una línea x-> len, entonces f (v) donde v está comprobado que es nulo se garantiza que se bloqueará. Además, si incluso antes se ha demostrado que v es nulo pero se ha comprobado que se llama f (v), tiene una contradicción lógica. Es lo mismo que tener a / b donde finalmente se demuestra que b es 0. Idealmente, dicho código no debería compilarse. Desactivar los controles de supuestos es completamente tonto a menos que el problema sea el costo de los controles, ya que oscurece el lugar donde se viola un supuesto. Al menos debe estar registrado. Debería tener un diseño de reinicio en caso de fallo.
Rob

22

El principio que sigo es el siguiente: si una situación puede evitarse de manera realista mediante la codificación, utilice una afirmación. De lo contrario, use una excepción.

Las afirmaciones son para garantizar que se cumpla el Contrato. El contrato debe ser justo, de modo que el cliente debe estar en condiciones de garantizar que cumple. Por ejemplo, puede indicar en un contrato que una URL debe ser válida porque las reglas sobre qué es y qué no es una URL válida son conocidas y coherentes.

Las excepciones son para situaciones que están fuera del control tanto del cliente como del servidor. Una excepción significa que algo ha salido mal, y no hay nada que se haya podido hacer para evitarlo. Por ejemplo, la conectividad de red está fuera del control de las aplicaciones, por lo que no se puede hacer nada para evitar un error de red.

Me gustaría agregar que la distinción Afirmación / Excepción no es realmente la mejor manera de pensarlo. En lo que realmente quiere pensar es en el contrato y cómo se puede hacer cumplir. En mi ejemplo de URL anterior, lo mejor que puede hacer es tener una clase que encapsule una URL y sea Null o una URL válida. Es la conversión de una cadena en una URL que hace cumplir el contrato, y se lanza una excepción si no es válida. Un método con un parámetro URL es mucho más claro que un método con un parámetro String y una aserción que especifica una URL.


6

Las afirmaciones son para detectar algo que un desarrollador ha hecho mal (no solo usted, sino también otro desarrollador de su equipo). Si es razonable que un error del usuario pueda crear esta condición, entonces debería ser una excepción.

Del mismo modo, piense en las consecuencias. Una afirmación normalmente cierra la aplicación. Si hay alguna expectativa realista de que la condición pueda recuperarse, probablemente debería usar una excepción.

Por otro lado, si el problema solo puede deberse a un error del programador, utilice una afirmación, porque desea saberlo lo antes posible. Una excepción puede ser atrapada y manejada, y nunca la descubrirás. Y sí, debe deshabilitar las afirmaciones en el código de lanzamiento porque allí desea que la aplicación se recupere si existe la más mínima posibilidad. Incluso si el estado de su programa está profundamente roto, el usuario podría guardar su trabajo.


5

No es exactamente cierto que "afirmar falla solo en modo de depuración".

En Construcción de software orientado a objetos, segunda edición de Bertrand Meyer, el autor deja una puerta abierta para verificar las condiciones previas en el modo de lanzamiento. En ese caso, lo que sucede cuando falla una aserción es que ... ¡se genera una excepción de violación de aserción! En este caso, no hay recuperación de la situación: sin embargo, se podría hacer algo útil, y es generar automáticamente un informe de error y, en algunos casos, reiniciar la aplicación.

La motivación detrás de esto es que las condiciones previas son generalmente más baratas de probar que las invariantes y las condiciones posteriores, y que en algunos casos la corrección y la "seguridad" en la versión de lanzamiento son más importantes que la velocidad. es decir, para muchas aplicaciones, la velocidad no es un problema, sino la robustez (la capacidad del programa de comportarse de manera segura cuando su comportamiento no es correcto, es decir, cuando se rompe un contrato).

¿Debería dejar siempre habilitados los controles de precondición? Depende. Tu decides. No hay una respuesta universal. Si está creando software para un banco, podría ser mejor interrumpir la ejecución con un mensaje alarmante que transferir $ 1,000,000 en lugar de $ 1,000. Pero, ¿y si estás programando un juego? Tal vez necesite toda la velocidad que pueda obtener, y si alguien obtiene 1000 puntos en lugar de 10 debido a un error que las condiciones previas no detectaron (porque no están habilitadas), mala suerte.

En ambos casos, idealmente debería haber detectado ese error durante la prueba, y debería hacer una parte significativa de su prueba con las aserciones habilitadas. Lo que se está discutiendo aquí es cuál es la mejor política para aquellos casos raros en los que las condiciones previas fallan en el código de producción en un escenario que no se detectó antes debido a pruebas incompletas.

Para resumir, puede tener afirmaciones y aún obtener las excepciones automáticamente , si las deja habilitadas, al menos en Eiffel. Creo que para hacer lo mismo en C ++, debe escribirlo usted mismo.

Ver también: ¿ Cuándo deben permanecer las afirmaciones en el código de producción?


1
Tu punto es definitivamente válido. El SO no especificó un idioma en particular - en el caso de C # la aserción estándar es System.Diagnostics.Debug.Assert, que no sólo no en una versión de depuración, y se elimina en tiempo de compilación en una versión de lanzamiento.
yoyo

2

Hubo un gran hilo con respecto a la habilitación / deshabilitación de aserciones en versiones de compilación en comp.lang.c ++. Moderado, que si tiene algunas semanas puede ver qué tan variadas son las opiniones al respecto. :)

Al contrario de coppro , creo que si no está seguro de que una aserción se pueda deshabilitar en una versión de lanzamiento, entonces no debería haber sido una aserción. Las afirmaciones son para proteger contra la invasión de los invariantes del programa. En tal caso, en lo que respecta al cliente de su código, habrá uno de dos posibles resultados:

  1. Muere con algún tipo de falla del tipo de sistema operativo, lo que resulta en una llamada para abortar. (Sin aseverar)
  2. Muere a través de una llamada directa al aborto. (Con afirmar)

No hay diferencia para el usuario, sin embargo, es posible que las afirmaciones agreguen un costo de rendimiento innecesario en el código que está presente en la gran mayoría de las ejecuciones donde el código no falla.

La respuesta a la pregunta en realidad depende mucho más de quiénes serán los clientes de la API. Si está escribiendo una biblioteca que proporciona una API, entonces necesita algún tipo de mecanismo para notificar a sus clientes que han utilizado la API incorrectamente. A menos que proporcione dos versiones de la biblioteca (una con aserciones y otra sin ellas), es muy poco probable que afirmar sea la opción adecuada.

Personalmente, sin embargo, tampoco estoy seguro de ir con excepciones para este caso. Las excepciones se adaptan mejor a dónde puede tener lugar una forma adecuada de recuperación. Por ejemplo, puede ser que esté intentando asignar memoria. Cuando detecta una excepción 'std :: bad_alloc', es posible liberar memoria y volver a intentarlo.


2

Describí mi punto de vista sobre el estado del asunto aquí: ¿Cómo se valida el estado interno de un objeto? . En general, haga valer sus reclamos y lance por violación de otros. Para deshabilitar afirmaciones en versiones de lanzamiento, puede hacer:

  • Desactivar afirmaciones para verificaciones costosas (como verificar si se ordena un rango)
  • Mantenga habilitadas las comprobaciones triviales (como la comprobación de un puntero nulo o un valor booleano)

Por supuesto, en las versiones de lanzamiento, las aserciones fallidas y las excepciones no detectadas deben manejarse de otra manera que no sea en las versiones de depuración (donde simplemente podría llamarse std :: abort). Escriba un registro del error en alguna parte (posiblemente en un archivo), dígale al cliente que ocurrió un error interno. El cliente podrá enviarle el archivo de registro.


1

está preguntando sobre la diferencia entre los errores de tiempo de diseño y tiempo de ejecución.

Las afirmaciones son notificaciones de 'oye programador, esto está roto', están ahí para recordarte los errores que no habrías notado cuando ocurrieron.

las excepciones son las notificaciones de 'oye usuario, algo salió mal' (obviamente, puedes codificar para atraparlas para que el usuario nunca sea informado) pero están diseñadas para ocurrir en tiempo de ejecución cuando el usuario de Joe está usando la aplicación.

Entonces, si crees que puedes eliminar todos tus errores, usa solo excepciones. Si crees que no puedes ... usa excepciones. Todavía puede usar afirmaciones de depuración para reducir el número de excepciones, por supuesto.

No olvide que muchas de las condiciones previas serán datos proporcionados por el usuario, por lo que necesitará una buena manera de informar al usuario que sus datos no fueron buenos. Para hacer eso, a menudo necesitará devolver los datos de error por la pila de llamadas a los bits con los que está interactuando. Las afirmaciones no serán útiles entonces, doblemente si su aplicación es n-tier.

Por último, no usaría ninguno: los códigos de error son muy superiores para los errores que cree que ocurrirán regularmente. :)


0

Prefiero el segundo. Si bien sus pruebas pueden haber funcionado bien, Murphy dice que algo inesperado saldrá mal. Entonces, en lugar de obtener una excepción en la llamada al método erróneo real, terminas rastreando una NullPointerException (o equivalente) 10 cuadros de pila más profundos.


0

Las respuestas anteriores son correctas: use excepciones para las funciones API públicas. El único momento en que puede desear romper esta regla es cuando la verificación es computacionalmente costosa. En ese caso, puedes ponerlo en una afirmación.

Si cree que es probable la violación de esa precondición, manténgala como una excepción o refactorice la precondición.


0

Deberías usar ambos. Las afirmaciones son para su conveniencia como desarrollador. Las excepciones detectan cosas que te perdiste o que no esperabas durante el tiempo de ejecución.

Me he aficionado a las funciones de informe de errores de glib en lugar de las simples afirmaciones antiguas. Se comportan como declaraciones de afirmación, pero en lugar de detener el programa, simplemente devuelven un valor y dejan que el programa continúe. Funciona sorprendentemente bien y, como beneficio adicional, puede ver qué sucede con el resto de su programa cuando una función no devuelve "lo que se supone que debe". Si se bloquea, sabe que su comprobación de errores es laxa en otro lugar en el futuro.

En mi último proyecto, utilicé este estilo de funciones para implementar la comprobación de condiciones previas, y si una de ellas fallaba, imprimiría un seguimiento de la pila en el archivo de registro pero seguiría ejecutándose. Me ahorró toneladas de tiempo de depuración cuando otras personas se encontrarían con un problema al ejecutar mi compilación de depuración.

#ifdef DEBUG
#define RETURN_IF_FAIL(expr)      do {                      \
 if (!(expr))                                           \
 {                                                      \
     fprintf(stderr,                                        \
        "file %s: line %d (%s): precondition `%s' failed.", \
        __FILE__,                                           \
        __LINE__,                                           \
        __PRETTY_FUNCTION__,                                \
        #expr);                                             \
     ::print_stack_trace(2);                                \
     return;                                                \
 };               } while(0)
#define RETURN_VAL_IF_FAIL(expr, val)  do {                         \
 if (!(expr))                                                   \
 {                                                              \
    fprintf(stderr,                                             \
        "file %s: line %d (%s): precondition `%s' failed.",     \
        __FILE__,                                               \
        __LINE__,                                               \
        __PRETTY_FUNCTION__,                                    \
        #expr);                                                 \
     ::print_stack_trace(2);                                    \
     return val;                                                \
 };               } while(0)
#else
#define RETURN_IF_FAIL(expr)
#define RETURN_VAL_IF_FAIL(expr, val)
#endif

Si necesitara una verificación de argumentos en tiempo de ejecución, haría esto:

char *doSomething(char *ptr)
{
    RETURN_VAL_IF_FAIL(ptr != NULL, NULL);  // same as assert(ptr != NULL), but returns NULL if it fails.
                                            // Goes away when debug off.

    if( ptr != NULL )
    {
       ...
    }

    return ptr;
}

No creo haber visto en la pregunta de OP nada relacionado con C ++. Creo que no debe incluirse en su respuesta.
ForceMagic

@ForceMagic: La pregunta tenía la etiqueta C ++ en 2008 cuando publiqué esta respuesta, y de hecho, la etiqueta C ++ se eliminó hace solo 5 horas. En cualquier caso, el código ilustra un concepto independiente del lenguaje.
indiv

0

Intenté sintetizar varias de las otras respuestas aquí con mis propios puntos de vista.

Utilice las aserciones para los casos en los que desee deshabilitarlo en producción, por error, para dejarlos dentro. La única razón real para deshabilitar en producción, pero no en desarrollo, es acelerar el programa. En la mayoría de los casos, esta aceleración no será significativa, pero a veces el código es crítico o la prueba es computacionalmente costosa. Si el código es de misión crítica, entonces las excepciones pueden ser mejores a pesar de la desaceleración.

Si hay alguna posibilidad real de recuperación, use una excepción ya que las aserciones no están diseñadas para recuperarse. Por ejemplo, el código rara vez está diseñado para recuperarse de errores de programación, pero está diseñado para recuperarse de factores tales como fallas de red o archivos bloqueados. Los errores no deben manejarse como excepciones simplemente por estar fuera del control del programador. Más bien, la previsibilidad de estos errores, en comparación con los errores de codificación, los hace más amigables para la recuperación.

Repita el argumento de que es más fácil depurar aserciones: el seguimiento de la pila de una excepción con el nombre adecuado es tan fácil de leer como una aserción. Un buen código solo debe capturar tipos específicos de excepciones, por lo que las excepciones no deben pasar desapercibidas debido a que se detectan. Sin embargo, creo que Java a veces te obliga a atrapar todas las excepciones.


0

La regla general, para mí, es usar expresiones de aserción para encontrar errores internos y excepciones para errores externos. Puede beneficiarse mucho de la siguiente discusión de Greg desde aquí .

Las expresiones de afirmación se utilizan para encontrar errores de programación: errores en la lógica del programa o errores en su implementación correspondiente. Una condición de aserción verifica que el programa permanece en un estado definido. Un "estado definido" es básicamente uno que está de acuerdo con los supuestos del programa. Tenga en cuenta que un "estado definido" para un programa no necesita ser un "estado ideal" o incluso "un estado habitual", o incluso un "estado útil", sino más sobre ese punto importante más adelante.

Para comprender cómo encajan las aserciones en un programa, considere una rutina en un programa C ++ que está a punto de desreferenciar un puntero. Ahora, ¿debería la rutina probar si el puntero es NULL antes de la desreferenciación, o debería afirmar que el puntero no es NULL y luego continuar y desreferenciarlo independientemente?

Me imagino que la mayoría de los desarrolladores querrían hacer ambas cosas, agregar la afirmación, pero también verificar el puntero para un valor NULO, a fin de no bloquearse si falla la condición afirmada. En la superficie, realizar la prueba y la verificación puede parecer la decisión más acertada

A diferencia de sus condiciones afirmadas, el manejo de errores de un programa (excepciones) se refiere no a errores en el programa, sino a entradas que el programa obtiene de su entorno. A menudo se trata de "errores" por parte de alguien, como un usuario que intenta iniciar sesión en una cuenta sin escribir una contraseña. Y a pesar de que el error puede evitar una finalización exitosa de la tarea del programa, no hay falla del programa. El programa no puede iniciar sesión en el usuario sin una contraseña debido a un error externo, un error por parte del usuario. Si las circunstancias eran diferentes, y el usuario ingresó la contraseña correcta y el programa no la reconoció; entonces, aunque el resultado seguiría siendo el mismo, la falla ahora pertenecería al programa.

El propósito del manejo de errores (excepciones) es doble. El primero es comunicar al usuario (o algún otro cliente) que se ha detectado un error en la entrada del programa y lo que significa. El segundo objetivo es restaurar la aplicación después de detectar el error, a un estado bien definido. Tenga en cuenta que el programa en sí no tiene errores en esta situación. Por supuesto, el programa puede estar en un estado no ideal, o incluso en un estado en el que no puede hacer nada útil, pero no hay error de programación. Por el contrario, dado que el estado de recuperación de error es uno anticipado por el diseño del programa, es uno que el programa puede manejar.

PD: es posible que desee consultar la pregunta similar: Excepción Vs Afirmación .


-1

Ver también esta pregunta :

En algunos casos, las afirmaciones están deshabilitadas cuando se compila para el lanzamiento. Es posible que no tenga control sobre esto (de lo contrario, podría construir con aserciones), por lo que sería una buena idea hacerlo de esta manera.

El problema con la "corrección" de los valores de entrada es que la persona que llama no obtendrá lo que espera, y esto puede provocar problemas o incluso bloqueos en partes completamente diferentes del programa, lo que hace que la depuración sea una pesadilla.

Por lo general, lanzo una excepción en la declaración if para asumir el papel de la afirmación en caso de que estén deshabilitados

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff
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.