El problema:
Desde hace mucho tiempo, estoy preocupado por el exceptions
mecanismo, porque siento que realmente no resuelve lo que debería.
RECLAMACIÓN: Hay largos debates afuera sobre este tema, y la mayoría de ellos tienen dificultades para comparar exceptions
vs devolver un código de error. Este definitivamente no es el tema aquí.
Intentando definir un error, estaría de acuerdo con CppCoreGuidelines, de Bjarne Stroustrup & Herb Sutter
Un error significa que la función no puede lograr su propósito anunciado
RECLAMACIÓN: El exception
mecanismo es un lenguaje semántico para manejar errores.
RECLAMACIÓN: Para mí, no hay "excusa" para que una función no logre una tarea: o definimos erróneamente las condiciones previas / posteriores para que la función no pueda garantizar resultados, o algún caso excepcional específico no se considera lo suficientemente importante como para pasar tiempo en el desarrollo una solución. Teniendo en cuenta que, en mi opinión, la diferencia entre el código normal y el manejo del código de error es (antes de la implementación) una línea muy subjetiva.
RECLAMACIÓN: El uso de excepciones para indicar cuándo no se mantiene una condición previa o posterior es otro propósito del exception
mecanismo, principalmente para fines de depuración. No apunto este uso de exceptions
aquí.
En muchos libros, tutoriales y otras fuentes, tienden a mostrar el manejo de errores como una ciencia bastante objetiva, que se resuelve exceptions
y solo se necesita catch
para tener un software robusto, capaz de recuperarse de cualquier situación. Pero mis varios años como desarrollador me hacen ver el problema desde un enfoque diferente:
- Los programadores tienden a simplificar su tarea lanzando excepciones cuando el caso específico parece demasiado raro para implementarlo con cuidado. Los casos típicos de esto son: problemas de falta de memoria, problemas de disco lleno, problemas de archivos corruptos, etc. Esto puede ser suficiente, pero no siempre se decide desde un nivel arquitectónico.
- Los programadores tienden a no leer cuidadosamente la documentación sobre las excepciones en las bibliotecas, y generalmente no saben qué y cuándo se lanza una función. Además, incluso cuando lo saben, realmente no los manejan.
- Los programadores tienden a no detectar excepciones lo suficientemente temprano, y cuando lo hacen, es principalmente para iniciar sesión y lanzar más. (consulte el primer punto).
Esto tiene dos consecuencias:
- Los errores que ocurren con frecuencia se detectan temprano en el desarrollo y se depuran (lo cual es bueno).
- Las excepciones raras no se gestionan y hacen que el sistema se bloquee (con un buen mensaje de registro) en la página de inicio del usuario. Algunas veces se informa el error, o ni siquiera.
Teniendo en cuenta que, el objetivo principal de un mecanismo de error de la OMI debería ser:
- Hacer visible en el código donde no se gestiona un caso específico.
- Comunique el tiempo de ejecución del problema al código relacionado (al menos la persona que llama) cuando ocurra esta situación.
- Proporciona mecanismos de recuperación.
El principal defecto de la exception
semántica como mecanismo de manejo de errores es la OMI: es fácil ver dónde throw
está a en el código fuente, pero no es absolutamente evidente saber si una función específica podría arrojarse al mirar la declaración. Esto trae todo el problema que presenté anteriormente.
El lenguaje no aplica y verifica el código de error tan estrictamente como lo hace para otros aspectos del lenguaje (por ejemplo, tipos fuertes de variables)
Un intento de solución
Con la intención de mejorar esto, desarrollé un sistema de manejo de errores muy simple, que intenta poner el manejo de errores en el mismo nivel de importancia que el código normal.
La idea es:
- Cada función (relevante) recibe una referencia a un
success
objeto muy ligero, y puede establecerlo en un estado de error en el caso. El objeto es muy ligero hasta que se guarda un error con el texto. - Se alienta a una función a omitir su tarea si el objeto proporcionado ya contiene un error.
- Un error nunca debe anularse.
El diseño completo obviamente considera a fondo cada aspecto (aproximadamente 10 páginas), también cómo aplicarlo a OOP.
Ejemplo de la Success
clase:
class Success
{
public:
enum SuccessStatus
{
ok = 0, // All is fine
error = 1, // Any error has been reached
uninitialized = 2, // Initialization is required
finished = 3, // This object already performed its task and is not useful anymore
unimplemented = 4, // This feature is not implemented already
};
Success(){}
Success( const Success& v);
virtual ~Success() = default;
virtual Success& operator= (const Success& v);
// Comparators
virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}
// Retrieve if the status is not "ok"
virtual bool operator!() const { return status!=ok;}
// Retrieve if the status is "ok"
operator bool() const { return status==ok;}
// Set a new status
virtual Success& set( SuccessStatus status, std::string msg="");
virtual void reset();
virtual std::string toString() const{ return stateStr;}
virtual SuccessStatus getStatus() const { return status; }
virtual operator SuccessStatus() const { return status; }
private:
std::string stateStr;
SuccessStatus status = Success::ok;
};
Uso:
double mySqrt( Success& s, double v)
{
double result = 0.0;
if (!s) ; // do nothing
else if (v<0.0) s.set(Error, "Square root require non-negative input.");
else result = std::sqrt(v);
return result;
}
Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;
Lo utilicé en muchos de mis (propios) códigos y obligó al programador (a mí) a pensar más sobre posibles casos excepcionales y cómo resolverlos (bien). Sin embargo, tiene una curva de aprendizaje y no se integra bien con el código que ahora lo usa.
La pregunta
Me gustaría entender mejor las implicaciones de usar tal paradigma en un proyecto:
- ¿Es correcta la premisa del problema? o ¿Me perdí algo relevante?
- ¿Es la solución una buena idea arquitectónica? o el precio es demasiado alto?
EDITAR:
Comparación entre métodos:
//Exceptions:
// Incorrect
File f = open("text.txt"); // Could throw but nothing tell it! Will crash
save(f);
// Correct
File f;
try
{
f = open("text.txt");
save(f);
}
catch( ... )
{
// do something
}
//Error code (mixed):
// Incorrect
File f = open("text.txt"); //Nothing tell you it may fail! Will crash
save(f);
// Correct
File f = open("text.txt");
if (f) save(f);
//Error code (pure);
// Incorrect
File f;
open(f, "text.txt"); //Easy to forget the return value! will crash
save(f);
//Correct
File f;
Error er = open(f, "text.txt");
if (!er) save(f);
//Success mechanism:
Success s;
File f;
open(s, "text.txt");
save(s, f); //s cannot be avoided, will never crash.
if (s) ... //optional. If you created s, you probably don't forget it.