He estado buscando una respuesta sólida a la pregunta de si se deben realizar o no verificaciones de tiempo de ejecución para validar las entradas con el fin de garantizar que un cliente se haya apegado a su final del acuerdo en el diseño por contrato. Por ejemplo, considere un constructor de clase simple:
class Foo
{
public:
Foo( BarHandle bar )
{
FooHandle handle = GetFooHandle( bar );
if( handle == NULL ) {
throw std::exception( "invalid FooHandle" );
}
}
};
Yo diría en este caso que un usuario no debe intentar construir un Foo
sin un válido BarHandle
. No parece correcto verificar que bar
sea válido dentro del Foo
constructor. Si simplemente documento que Foo
el constructor requiere un válido BarHandle
, ¿no es suficiente? ¿Es esta una forma adecuada de hacer cumplir mi condición previa en el diseño por contrato?
Hasta ahora, todo lo que he leído tiene opiniones encontradas sobre esto. Parece que el 50% de las personas diría que verifica que bar
es válido, el otro 50% diría que no debería hacerlo, por ejemplo, considere un caso en el que el usuario verifique que BarHandle
es correcto, pero una segunda verificación (e innecesaria) también se está haciendo dentro del Foo
constructor de.
fuente
Respuestas:
No creo que haya una sola respuesta para esto. Creo que lo principal que es necesario es la coherencia, ya sea que imponga todas las condiciones previas en una función o no intente imponer ninguna de ellas. Desafortunadamente, eso es bastante raro: lo que generalmente sucede es que, en lugar de pensar en las condiciones previas y hacerlas cumplir, los programadores agregan fragmentos de código para hacer cumplir las condiciones previas cuya violación causó fallas durante las pruebas, pero con frecuencia dejan abiertas otras posibilidades que pueden causar fallas, pero no surgió en las pruebas.
En muchos casos, es bastante razonable proporcionar dos capas: una para uso "interno" que no intenta hacer cumplir ninguna condición previa, y luego una segunda para uso "externo" que solo impone condiciones previas, luego invoca la primera.
Sin embargo, creo que es mejor hacer cumplir las condiciones previas en el nodo de origen, no solo documentarlas. Una excepción o afirmación es mucho más difícil de ignorar que la documentación y es mucho más probable que permanezca sincronizado con el resto del código.
fuente
foo
es NULL, pero ser NULL no es la única forma en quefoo
podría ser inválido. Por ejemplo, ¿qué pasa con -1 elenco aFooHandle
? No puedo verificar todas las formas posibles en que el identificador podría ser inválido. NULL es una opción obvia y es algo que normalmente se verifica, pero no es una verificación concluyente. ¿Qué recomendarías aquí?Es una pregunta muy difícil, porque hay varios conceptos diferentes:
Sin embargo, esto es principalmente un artefacto de una falla de tipo , en este caso. La nulidad se aplica mejor mediante restricciones de tipo, porque el compilador realmente las verifica. Aún así, dado que no todo se puede capturar en un sistema de tipos, especialmente en C ++, la pregunta en sí misma todavía vale la pena.
Personalmente, creo que la corrección y la documentación son primordiales. Ser rápido e incorrecto es inútil. Ser rápido y estar solo mal a veces es un poco mejor, pero tampoco aporta mucho a la mesa.
Sin embargo, el rendimiento puede ser crítico en algunas partes de los programas, y algunas comprobaciones pueden ser bastante extensas (es decir, demostrar que un gráfico dirigido tiene todos sus nodos accesibles y co-accesibles). Por lo tanto, votaría por un enfoque dual.
Principio uno: Fail Fast . Este es un principio rector en la programación defensiva en general, que aboga por la detección de errores en la etapa más temprana posible. Añadiría Fail Hard a la ecuación.
Desafortunadamente, en un entorno de producción, fallar mucho no es necesariamente la mejor solución. En este caso, una excepción específica puede ayudar a salir de allí a toda prisa, y dejar que algún manejador de alto nivel se dé cuenta y aborde el caso fallido de manera adecuada (lo más probable es iniciar sesión y seguir adelante con un nuevo caso).
Sin embargo, esto no aborda el problema de las pruebas costosas . En los puntos calientes, esas pruebas pueden costar demasiado. En este caso, es razonable habilitar solo la prueba en las compilaciones DEBUG.
Esto nos deja con una solución agradable y simple:
SOFT_ASSERT(Cond_, Text_)
DEBUG_ASSERT(Cond_, Text_)
Donde las dos macros se definen así:
fuente
Una cita que he escuchado sobre esto es:
"Sea conservador en lo que hace y liberal en lo que acepta".
Lo que se reduce a seguir los contratos para los argumentos cuando se llaman funciones, y verificar todas las entradas antes de actuar cuando se escriben funciones.
En última instancia, depende del dominio. Si está haciendo una API de sistema operativo, será mejor que verifique cada entrada, no confíe en que todos los datos entrantes sean válidos antes de comenzar a actuar sobre ella. Si está haciendo una biblioteca para que otros la usen, continúe, deje que el usuario se atornille (OpenGL viene a su mente primero por alguna razón desconocida).
EDITAR: en el sentido OO, parece haber dos enfoques: uno que dice que un objeto nunca debe estar malformado (todos sus invariantes deben ser verdaderos) durante todo el tiempo que un objeto es accesible, y otro enfoque que dice que tiene un constructor que no establece todos los invariantes, luego establece algunos valores más y tiene una segunda función de inicialización que finaliza init.
Me gusta más el primero, ya que no requiere conocimientos mágicos ni depende de la documentación actual para saber qué partes de inicialización no hace el constructor.
fuente