c++ - example - Teoría sobre el manejo de errores?
catch exception c++ example (14)
¿Está registrando algo que solo debería hacerse en el código de la aplicación? ¿O está bien hacer algo de registro desde el código de la biblioteca?
Solo quería comentar sobre esto. Mi opinión es que nunca se registre directamente en el código de la biblioteca, sino que proporcione ganchos o devoluciones de llamada para implementar esto en el código de la aplicación, de modo que la aplicación pueda decidir qué hacer con el resultado del registro (si hay algo).
La mayoría de los consejos sobre el manejo de errores se reduce a un puñado de consejos y trucos (ver esta publicación, por ejemplo). Estas sugerencias son útiles, pero creo que no responden todas las preguntas. Siento que debo diseñar mi aplicación de acuerdo con una cierta filosofía, una escuela de pensamiento que proporciona una base sólida sobre la cual construir. ¿Existe tal teoría sobre el tema del manejo de errores?
Aquí hay algunas preguntas prácticas:
- ¿Cómo decidir si un error debe manejarse localmente o propagarse a un código de nivel superior?
- ¿Cómo decidir entre registrar un error o mostrarlo como un mensaje de error para el usuario?
- ¿Está registrando algo que solo debería hacerse en el código de la aplicación? ¿O está bien hacer algo de registro desde el código de la biblioteca?
- En caso de excepciones, ¿dónde debería generalmente atraparlos? En el código de nivel bajo o superior?
- Debería esforzarse por una estrategia unificada de manejo de errores a través de todas las capas de código, o tratar de desarrollar un sistema que pueda adaptarse a una variedad de estrategias de manejo de errores (para poder manejar los errores de bibliotecas de terceros).
- ¿Tiene sentido crear una lista de códigos de error? ¿O está pasado de moda en estos días?
En muchos casos, el sentido común es suficiente para desarrollar una estrategia lo suficientemente buena para tratar las condiciones de error. Sin embargo, me gustaría saber si existe un enfoque más formal / "académico".
PD: esta es una pregunta general, pero las respuestas específicas de C ++ también son bienvenidas (C ++ es mi principal lenguaje de programación para el trabajo).
Introducción
Para entender lo que se necesita hacer para manejar el error, creo que uno necesita comprender claramente los tipos de errores que uno encuentra y los contextos en los que uno los encuentra.
Para mí, ha sido extremadamente útil considerar los dos principales tipos de errores como:
Errores que nunca deberían ocurrir, y normalmente se deben a un error en el código.
Errores que se prevén y no se pueden evitar durante el funcionamiento normal, como una conexión de base de datos que no funciona debido a un problema de la base de datos sobre el cual la aplicación no tiene control.
La forma en que se debe manejar un error depende en gran medida del tipo de error que sea.
Los diferentes contextos que afectan cómo se deben manejar los errores son:
Código de aplicación
Código de biblioteca
El manejo de errores en el código de la biblioteca difiere algo del manejo en el código de la aplicación.
Una filosofía para el manejo de los dos principales tipos de errores se analiza a continuación. Las consideraciones especiales para el código de la biblioteca también se abordan. Finalmente, las preguntas prácticas específicas en la publicación original se abordan en el contexto de la filosofía presentada.
Tipos de errores
Errores de programación, errores y otros errores que nunca deberían pasar
Muchos errores son el resultado de errores de programación. Estos errores generalmente no se pueden corregir, ya que no se puede anticipar el error de programación específico. Eso significa que no podemos saber de antemano en qué condición el error deja la aplicación, por lo que no podemos recuperarnos de esa condición y no debemos intentarlo.
En última instancia, la solución a este tipo de error es corregir el error de programación. Para facilitar eso, el error debe aparecer lo más rápido posible. Idealmente, el programa debería salir inmediatamente después de identificar tal error y proporcionar la información relevante. Una salida rápida y obvia reduce el tiempo requerido para completar el ciclo de depuración y repetición de pruebas, lo que permite corregir más errores en la misma cantidad de tiempo de prueba; eso a su vez resulta en una aplicación más robusta con menos errores cuando llega el momento de implementar.
El otro objetivo principal en el manejo de este tipo de error debe ser proporcionar información suficiente para facilitar la identificación del error. En Java, por ejemplo, lanzar una RuntimeException a menudo proporciona suficiente información en el seguimiento de la pila para identificar el error inmediatamente; en código limpio, las soluciones inmediatas a menudo se pueden identificar simplemente al examinar el seguimiento de la pila. En otros idiomas, uno puede registrar la pila de llamadas o conservar la información necesaria. Es fundamental no suprimir la información en aras de la brevedad; no se preocupe por la cantidad de espacio de registro que está ocupando cuando se produzca este tipo de error. Cuanta más información se proporcione, más rápido podrán corregirse los errores, y habrá menos errores para contaminar los registros cuando la aplicación llegue a la producción.
Aplicaciones de servidor
Ahora, en algunas aplicaciones de servidor, es importante que el servidor sea lo suficientemente tolerante a fallas para continuar operando incluso ante errores de programación ocasionales. En este caso, el mejor enfoque es tener una separación muy clara entre el código del servidor que debe continuar funcionando y el código de procesamiento de la tarea que se puede permitir que falle. Por ejemplo, las tareas pueden relegarse a subprocesos o subprocesos, como se hace en muchos servidores web.
En una arquitectura de servidor de este tipo, el subproceso o subproceso que maneja la tarea se puede tratar como una aplicación que puede fallar. Todas las consideraciones anteriores se aplican a dicha tarea: el error debe emerger lo más rápidamente posible mediante una salida limpia de la tarea, y se debe registrar suficiente información para permitir que el error se encuentre y se solucione fácilmente. Cuando tal tarea finaliza en Java, por ejemplo, la traza de la pila completa de cualquier RuntimeException que causa la salida normalmente debe registrarse.
La mayor parte del código posible se debe ejecutar dentro de los subprocesos o procesos que manejan la tarea, en lugar de en el subproceso o proceso del servidor principal. Esto se debe a que cualquier error en la secuencia o proceso del servidor principal seguirá causando la caída de todo el servidor. Es mejor insertar el código (con los errores que contiene) en el código de manejo de tareas donde no causará un bloqueo del servidor cuando el error se manifieste.
Errores que se esperan y no se pueden evitar en el funcionamiento normal
Los errores que se esperan y no se pueden evitar en el funcionamiento normal, como una excepción de una base de datos u otro servicio separado de la aplicación, requieren un tratamiento muy diferente. En estos casos, el objetivo no es corregir el código, sino hacer que el código maneje el error cuando eso tenga sentido, e informar a los usuarios u operadores que pueden solucionar el problema de otra manera.
En estos casos, por ejemplo, la aplicación puede desechar cualquier resultado que se haya acumulado hasta el momento y volver a intentar la operación. En el acceso a la base de datos, el uso de transacciones puede ayudar a garantizar que los datos acumulados se descarten. En otros casos, puede ser útil escribir el código de uno con tales reintentos en mente. El concepto de idempotencia también puede ser útil aquí.
Cuando los intentos automáticos no resuelvan el problema lo suficiente, los seres humanos deben ser informados. El usuario debe ser informado de que la operación falló; a menudo, al usuario se le puede dar la opción de volver a intentarlo. El usuario puede entonces juzgar si es conveniente realizar un reintento y también puede modificar las entradas que podrían ayudar a que las cosas funcionen mejor en un nuevo intento.
Para este tipo de error, el registro y tal vez los avisos por correo electrónico se pueden utilizar para informar a los operadores del sistema. A diferencia del registro de errores de programación, el registro de errores que se esperan en una operación normal debería ser más breve, ya que el error puede ocurrir muchas veces y aparecer muchas veces en los registros; los operadores a menudo analizarán el patrón de muchos errores, en lugar de centrarse en un error individual.
Bibliotecas y aplicaciones
La discusión anterior sobre los tipos de errores es directamente aplicable al código de la aplicación. El otro contexto principal para el manejo de errores es el código de la biblioteca. El código de la biblioteca todavía tiene los mismos dos tipos básicos de errores, pero normalmente no puede o no debe comunicarse directamente con el usuario, y tiene menos conocimiento sobre el contexto de la aplicación, incluso si es aceptable una salida inmediata, que el código de la aplicación.
Como resultado, existen diferencias en la forma en que las bibliotecas deben manejar el registro, cómo deben manejar los errores que se pueden esperar en el funcionamiento normal y cómo deben manejar los errores de programación y otros errores que nunca deberían ocurrir.
Con respecto al registro, la biblioteca debería admitir, de ser posible, el registro en el formato deseado por el código de la aplicación cliente. Un enfoque válido es no hacer ningún registro, y permitir que el código de la aplicación haga todo el registro basado en la información de error proporcionada al código de la aplicación por la biblioteca. Otro enfoque es usar una interfaz de registro configurable, que permita a la aplicación del cliente proporcionar la implementación para el registro, por ejemplo, cuando la biblioteca se carga por primera vez. En Java, por ejemplo, la biblioteca puede usar la interfaz de registro de inicio de sesión y permitir que la aplicación se preocupe por la implementación de inicio de sesión que debe configurar para que logback lo use.
Para errores y otros errores que nunca deberían ocurrir, las bibliotecas todavía no pueden simplemente salir de la aplicación, ya que puede no ser aceptable para la aplicación. Por el contrario, las bibliotecas deben salir de la llamada a la biblioteca, proporcionando a la persona que llama la información suficiente para ayudar a diagnosticar el problema. La información se puede proporcionar en forma de una excepción con un seguimiento de pila, o la biblioteca puede registrar la información si se utiliza el enfoque de registro configurable. La aplicación puede tratar esto como lo haría con cualquier otro error de este tipo, normalmente al salir, o en un servidor, al permitir que el proceso o subproceso de tarea salga, con el mismo registro o informe de errores que se realizaría para los errores de programación en el código de la aplicación.
Los errores que se esperan en la operación normal también se deben informar al código del cliente. En este caso, como con este tipo de error cuando se encuentra en el código del cliente, la información asociada con el error puede ser más sucinta. Normalmente, las bibliotecas deberían hacer un manejo menos local de este tipo de error, confiando más en el código del cliente para decidir cosas como si volver a intentarlo y cuántas veces. El código del cliente puede pasar la decisión de reintento al usuario si así lo desea.
Preguntas practicas
Ahora que tenemos la filosofía, vamos a aplicarla a las preguntas prácticas que mencionas.
- ¿Cómo decidir si un error debe manejarse localmente o propagarse a un código de nivel superior?
Si se espera un error en la operación normal, vuelva a intentar o posiblemente consulte al usuario localmente. De lo contrario, propagalo a un código de nivel superior.
- ¿Cómo decidir entre registrar un error o mostrarlo como un mensaje de error para el usuario?
Si se espera un error en la operación normal, y la entrada del usuario sería útil para determinar qué acción tomar, obtener la entrada del usuario y registrar un mensaje breve; si parece ser un error de programación, proporcione al usuario una breve notificación y registre información más extensa.
- ¿Está registrando algo que solo debería hacerse en el código de la aplicación? ¿O está bien hacer algo de registro desde el código de la biblioteca?
El registro desde el código de la biblioteca debe estar bajo el control del código del cliente. Como máximo, la biblioteca debe iniciar sesión en una interfaz para la cual el cliente proporciona la implementación.
- En caso de excepciones, ¿dónde debería generalmente atraparlos? En el código de nivel bajo o superior?
Las excepciones que se esperan en el funcionamiento normal se pueden capturar localmente y la operación se puede reintentar o manejar de otra forma. En todos los demás casos, se debe permitir que las excepciones se propaguen.
- Debería esforzarse por una estrategia unificada de manejo de errores a través de todas las capas de código, o tratar de desarrollar un sistema que pueda adaptarse a una variedad de estrategias de manejo de errores (para poder manejar los errores de bibliotecas de terceros).
Los tipos de errores en bibliotecas de terceros son los mismos tipos de errores que ocurren en el código de la aplicación. Los errores se deben manejar principalmente según el tipo de error que representan, con ajustes relevantes para el código de la biblioteca.
- ¿Tiene sentido crear una lista de códigos de error? ¿O está pasado de moda en estos días?
El código de la aplicación debe proporcionar una descripción completa del error en el caso de errores de programación, y una descripción sucinta en el caso de errores que pueden ocurrir en la operación normal; en cualquier caso, una descripción es normalmente más apropiada que un código de error. Las bibliotecas pueden proporcionar un código de error como una forma de describir si un error es una programación u otro error interno, o si el error es uno que puede ocurrir en la operación normal, con el último tipo tal vez subdividido más finamente; sin embargo, una jerarquía de excepciones puede ser más útil que un código de error en los idiomas donde esto es posible. Sin embargo, tenga en cuenta que las aplicaciones que se ejecutan desde la línea de comandos pueden actuar como bibliotecas para scripts de shell.
Siempre maneje lo antes posible. Cuanto más cerca esté de su ocurrencia, más posibilidades tendrá de hacer algo significativo o, al menos, descubrir dónde y por qué sucedió. En C ++, no es solo una cuestión de contexto sino que es imposible de determinar en muchos casos.
En general, siempre debes detener la aplicación si ocurre un error que es un error real (no algo así como no encontrar un archivo, que no es realmente algo que debe contar como un error, sino que está etiquetado como tal). No se resolverá solo, y una vez que se rompa la aplicación causará errores que son imposibles de depurar porque no tienen nada que ver con el área donde ocurren.
Por qué no?
ver 1.
ver 1.
Debes mantener las cosas simples, o te arrepentirás. Más importante para manejar errores en el tiempo de ejecución es probarlos para evitarlos.
Es como decir que es mejor centralizar o no centralizar. Puede tener mucho sentido en algunos casos, pero puede ser una pérdida de tiempo en otros. Para algo que es un lib / módulo cargable de algún tipo que puede tener errores que están relacionados con datos (basura, basura), tiene mucho sentido. Para un manejo de error más general o errores catastróficos, menos.
El libro "Pautas de diseño del marco: Convenciones, modismos y patrones para bibliotecas .NET reutilizables" de Krzysztof Cwalina y Brad Abrams tiene algunas buenas sugerencias al respecto. Ver el capítulo 7 sobre Excepciones. Por ejemplo, favorece arrojar excepciones a devolver códigos de error.
-Krip
El manejo de errores no va acompañado de una teoría formal. También es "específico para la implementación" de un tema para ser considerado un campo de la ciencia (para ser justos, hay un gran debate sobre si la programación es una ciencia por sí misma).
Sin embargo, es una buena parte del trabajo de un desarrollador (y por lo tanto de su vida), por lo que se han desarrollado enfoques prácticos y una guía técnica sobre el tema.
Una buena visión sobre el tema es presentada por A. Alexandrescu, en su charla manejo de errores sistemáticos en C ++
Tengo un repositorio en GitHub donde se implementan las técnicas presentadas.
Básicamente, lo que hace AA es implementar una clase
template<class T>
class Expected { /* Implementation in the GitHub link */ };
que está destinado a ser utilizado como un valor de retorno. Esta clase podría contener un valor de devolución de tipo T
o una excepción (puntero). La excepción podría ser lanzada explícitamente o bajo petición, sin embargo, la información de error abundante siempre está disponible. Un ejemplo de uso sería como esto
int foo();
// ....
Expected<int> ret = foo();
if (ret.valid()) {
// do the work
}
else {
// either use the info of the exception
// or throw the exception (eg in an exception "friendly" codebase)
}
Al construir este marco para el manejo de errores, AA nos guía a través de técnicas y diseños que producen un manejo de errores exitoso o deficiente, y qué funciona o no. También da sus definiciones de ''error'' y ''manejo de errores''
Estoy cambiando mi filosofía de diseño y codificación para que:
- Si todo funciona sin problemas, como se esperaba, no se generan errores.
- Lanza una excepción si ocurre algo diferente o inesperado; deja que la persona que llama lo maneje.
- Si no se puede resolver, propagalo a un nivel superior.
Con suerte, con esta técnica, los problemas que se propagan al usuario serán muy importantes; de lo contrario, el programa intenta resolverlos.
Actualmente estoy experimentando problemas que se pierden en los códigos de retorno; o se crean nuevos códigos de retorno.
Hace un par de años, pensé exactamente sobre la misma pregunta :)
Después de buscar y leer varias cosas, creo que la referencia más interesante que encontré fue Patterns for Generation, Handling and Management of Errors de Andy Longshaw y Eoin Woods. Es un intento breve y sistemático de cubrir los modismos básicos que mencionas y algunos otros.
La respuesta a estas preguntas es bastante controvertida, pero los autores anteriores fueron lo suficientemente valientes como para exponerse a sí mismos en una conferencia, y luego poner sus pensamientos en el papel.
La primera pregunta es, probablemente, ¿qué puedes hacer con el error?
¿Puedes arreglarlo (en cuyo caso debes decirle al usuario) o puede el usuario arreglarlo?
Si nadie puede solucionarlo y usted va a salir, ¿hay algún valor para que se le informe a usted (a través de un volcado de emergencia o un código de error)?
Mi opinión sobre el registro (u otras acciones) desde el código de la biblioteca es NUNCA.
Una biblioteca no debe imponer ninguna política a su usuario, y el usuario puede haber INTENTADO que ocurra un error. Quizás el programa estaba solicitando deliberadamente un error en particular, a la espera de que llegara, para probar alguna condición. Registrar este error sería engañoso.
El registro (o cualquier otra cosa) impone una política a la persona que llama, lo que es malo. Además, si una condición de error inofensivo (que la persona que realiza la llamada ignorara o reintentara inofensivamente, por ejemplo) ocurriera con una alta frecuencia, el volumen de registros podría enmascarar cualquier error legítimo o causar problemas de robustez (llenar discos, usar IO excesivo etc)
Mis dos centavos.
¿Cómo decidir si un error debe manejarse localmente o propagarse a un código de nivel superior? Controle los errores que puede manejar. Deje que los errores se propaguen que no puede.
¿Cómo decidir entre registrar un error o mostrarlo como un mensaje de error para el usuario? Dos problemas ortogonales, que no son mutuamente excluyentes. Iniciar sesión el error es en última instancia para usted, el desarrollador. Si estuviera interesado en él, conéctelo. Muéstrelo al usuario si es procesable para el usuario ("Error: ¡Sin conexión a la red!").
¿Está registrando algo que solo debería hacerse en el código de la aplicación? ¿O está bien hacer algo de registro desde el código de la biblioteca? No veo ninguna razón por la cual las bibliotecas no puedan iniciar sesión.
En caso de excepciones, ¿dónde debería generalmente atraparlos? En el código de nivel bajo o superior? Debes atraparlos donde puedas manejarlos (inserta tu definición de manejador). Si no puede manejarlos, ignórelos (tal vez alguien de la pila pueda manejarlos ...).
Ciertamente no deberías poner un bloque try / catch alrededor de todas y cada una de las funciones de lanzamiento que llamas.
Pregunta similar: ¿en qué punto debería dejar de propagar un error y tratar con él? Debería esforzarse por una estrategia unificada de manejo de errores a través de todas las capas de código, o tratar de desarrollar un sistema que pueda adaptarse a una variedad de estrategias de manejo de errores (para poder manejar los errores de bibliotecas de terceros). En el primer punto, puedes enfrentarlo. Es posible que ese punto no exista y que tu aplicación se bloquee. Luego obtendrás un buen volcado de emergencia y podrás actualizar tu manejo de errores.
¿Tiene sentido crear una lista de códigos de error? ¿O está pasado de moda en estos días? Otro punto de discordia. De hecho, diría que no: una lista súper de todos los códigos de error implica que esa lista siempre está actualizada, por lo que puede hacer daño cuando no está actualizado. Es mejor que cada función documente todos los códigos de error que puede devolver, en lugar de tener una lista súper.
Descargo de responsabilidad: no conozco ninguna teoría sobre el manejo de errores, sin embargo, pensé repetidamente sobre el tema mientras exploraba varios lenguajes y paradigmas de programación, y también jugaba con los diseños de lenguaje de programación (y los discutía). Lo que sigue, por lo tanto, es un resumen de mi experiencia hasta el momento; con argumentos objetivos.
Nota: esto debería cubrir todas las preguntas, pero ni siquiera traté de abordarlas en orden, prefiriendo en su lugar una presentación estructurada. Al final de cada sección, presento una respuesta sucinta a las preguntas que respondió, para mayor claridad.
Introducción
Como premisa, me gustaría señalar que, independientemente de lo que esté sujeto a discusión, algunos parámetros deben tenerse en cuenta al diseñar una biblioteca (o código reutilizable).
El autor no puede pretender comprender cómo se usará esta biblioteca y, por lo tanto, debe evitar las estrategias que dificultan la integración de lo que debería. El defecto más flagrante sería confiar en un estado compartido globalmente; el estado compartido de subproceso local también puede ser una pesadilla para las interacciones con coroutines / green-threads. El uso de tales corrutinas y subprocesos también resalta que la sincronización se deja mejor al usuario, en código de un solo subproceso significará ninguno (mejor rendimiento), mientras que en corutinas y subprocesos verdes el usuario es el más adecuado para implementar (o usar implementaciones de) mecanismos de sincronización dedicados.
Dicho esto, cuando la biblioteca es solo para uso interno, las variables globales o locales pueden ser convenientes; si se usan, deben documentarse claramente como una limitación técnica.
Explotación florestal
Hay muchas formas de registrar mensajes:
- con información adicional como la marca de tiempo, la identificación del proceso, la identificación del hilo, el nombre del servidor / IP, ...
- a través de llamadas síncronas o con un mecanismo asíncrono (y un mecanismo de manejo de desbordamiento)
- en archivos, bases de datos, bases de datos distribuidas, servidores de registro dedicados, ...
Como autor de una biblioteca, los registros deben estar integrados dentro de la infraestructura del cliente (o apagados). Esto se proporciona mejor al permitir que el cliente proporcione ganchos para que se ocupen de los registros, mi recomendación es:
- para proporcionar 2 ganchos: uno para decidir si desea iniciar sesión o no, y uno para realmente iniciar sesión (el mensaje está formateado y el último gancho llamado solo cuando el cliente decidió iniciar sesión)
- para proporcionar, en la parte superior del mensaje: una gravedad (nivel aka), el nombre del archivo, la línea y el nombre de la función si es de código abierto o de lo contrario el módulo lógico (si hay varios)
- para, de forma predeterminada, escribir en
stdout
ystderr
(según la gravedad), hasta que el cliente indique explícitamente no iniciar sesión
Me gustaría señalar que, siguiendo las pautas delineadas en la introducción, la sincronización se deja al cliente.
En cuanto a si registrar errores: no inicie sesión (como errores) lo que de otra manera ya informaría a través de su API; sin embargo, aún puede registrar con menor gravedad los detalles. El cliente puede decidir si informa o no al manejar el error y, por ejemplo, opta por no informarlo si fue solo una llamada especulativa.
Nota: cierta información no debe aparecer en los registros y algunas otras piezas están mejor ocultas. Por ejemplo, las contraseñas no deben registrarse, y los números de la tarjeta de crédito o pasaporte / seguro social están mejor ocultos (al menos en parte). En una biblioteca diseñada para tal información sensible, esto se puede hacer durante el registro; de lo contrario, la aplicación debería encargarse de esto.
¿Está registrando algo que solo debería hacerse en el código de la aplicación? ¿O está bien hacer algo de registro desde el código de la biblioteca?
El código de la aplicación debe decidir la política. Si una biblioteca registra o no depende de si es necesario.
¿Continuando después de un error?
Antes de que realmente hablemos sobre la notificación de errores, la primera pregunta que debemos hacernos es si el error debe ser informado (para el manejo) o si las cosas están tan mal que abortar el proceso actual es claramente la mejor política.
Este es ciertamente un tema complicado. En general, aconsejaría diseñar de modo que continuar es una opción, con una purga / reinicio si es necesario. Si esto no se puede lograr en ciertos casos, entonces esos casos deberían provocar un aborto del proceso.
Nota: en algunos sistemas, es posible obtener un volcado de memoria del proceso. Si una aplicación maneja datos confidenciales (contraseña, tarjetas de crédito, pasaportes, ...), es mejor desactivarla en producción (pero se puede usar durante el desarrollo).
Nota: puede ser interesante tener un conmutador de depuración que transforma una parte de las llamadas de informes de errores en abortos con un volcado de memoria para ayudar a la depuración durante el desarrollo.
Reportar un error
La aparición de un error significa que el contrato de una función / interfaz no se pudo cumplir. Esto tiene varias consecuencias:
- el cliente debe ser advertido, por lo que el error debe ser informado
- datos parcialmente correctos deberían escapar en la naturaleza
El último punto será tratado más adelante; por ahora, centrémonos en informar el error. El cliente no debe, nunca, ignorar accidentalmente este informe. Por eso, el uso de códigos de error es una abominación (en los idiomas en los que los valores de retorno pueden ignorarse):
ErrorStatus_t doit(Input const* input, Output* output);
Conozco dos esquemas que requieren una acción explícita en la parte del cliente:
- excepciones
- tipos de resultados (
optional<T>
,either<T, U>
, ...)
El primero es bien conocido, este último se usa mucho en los lenguajes funcionales y se introdujo en C ++ 11 con el pretexto de std::future<T>
aunque existen otras implementaciones.
Recomiendo preferir este último, cuando sea posible, ya que es más fácil de entender, pero volver a las excepciones cuando no se espera ningún resultado. Contraste:
Option<Value&> find(Key const&);
void updateName(Client::Id id, Client::Name name);
En el caso de operaciones "solo de escritura" como updateName
, el cliente no tiene uso para un resultado. Podría ser introducido, pero sería fácil olvidar el cheque.
La reversión a excepciones también se produce cuando un tipo de resultado no es práctico o es insuficiente para transmitir los detalles:
Option<Value&> compute(RepositoryInterface&, Details...);
En tal caso de devolución de llamada externamente definida, hay una lista casi infinita de fallas potenciales. La implementación podría usar la red, una base de datos, el sistema de archivos, ... en este caso, y para informar errores con precisión:
- se debe esperar que la devolución de llamada definida externamente reporte errores a través de excepciones cuando la interfaz es insuficiente (o poco práctica) para transmitir los detalles completos del error.
- las funciones basadas en esta devolución de llamada abstracta deberían ser transparentes para esas excepciones (dejarlas pasar, sin modificar)
El objetivo es permitir que esta excepción salte a la capa donde se decidió la implementación de la interfaz (al menos), ya que solo en este nivel existe la posibilidad de interpretar correctamente la excepción lanzada.
Nota: la devolución de llamada externamente definida no está obligada a usar excepciones, solo debemos esperar que pueda estar usando algunas.
Usando un error
Para usar un informe de error, el cliente necesita suficiente información para tomar una decisión. Se debe preferir la información estructurada, como los códigos de error o los tipos de excepción (para acciones automáticas) y se puede proporcionar información adicional (mensaje, pila, ...) de una manera no estructurada (para que los humanos investiguen).
Sería mejor si una función documentara claramente todos los posibles modos de falla: cuándo ocurren y cómo se informan. Sin embargo, especialmente en caso de que se ejecute código arbitrario, el cliente debe estar preparado para tratar con códigos / excepciones desconocidos.
Una excepción notable es, por supuesto, los tipos de resultados: boost::variant<Output, Error0, Error1, ...>
proporciona una lista exhaustiva comprobada por el compilador de los modos de falla conocidos ... aunque una función que devuelva este tipo aún podría arrojarse, por supuesto.
¿Cómo decidir entre registrar un error o mostrarlo como un mensaje de error para el usuario?
El usuario siempre debe ser advertido cuando su orden no se puede cumplir, sin embargo, debe mostrarse un mensaje fácil de usar (comprensible). Si es posible, también se deben presentar consejos o soluciones alternativas. Los detalles son para equipos de investigación.
¿Recuperarse de un error?
Por último, pero ciertamente no menos importante, viene la parte verdaderamente aterradora sobre los errores: la recuperación.
Esto es algo para lo que las bases de datos (las reales) son tan buenas: una semántica similar a la transacción. Si ocurre algo inesperado, la transacción se cancela como si nada hubiera sucedido.
En el mundo real, las cosas no son simples. El simple ejemplo de cancelar un correo electrónico enviado a casa se me viene a la mente: demasiado tarde. Los protocolos pueden existir, dependiendo del dominio de su aplicación, pero esto está fuera de esta discusión. El primer paso, sin embargo, es la capacidad de recuperar un estado sano en la memoria ; y eso está lejos de ser simple en la mayoría de los idiomas (y STM solo puede hacer mucho hoy).
En primer lugar, una ilustración del desafío:
void update(Client& client, Client::Name name, Client::Address address) {
client.update(std::move(name));
client.update(std::move(address)); // Throws
}
Ahora, después de actualizar la dirección fallida, me queda un client
medio actualizado. Que puedo hacer ?
- intentar deshacer todas las actualizaciones que ocurrieron es casi imposible (la operación de deshacer podría fallar)
- copiar el estado antes de ejecutar cualquier actualización individual es un rendimiento alto (suponiendo que podamos incluso intercambiarlo de una manera segura)
En cualquier caso, la contabilidad requerida es tal que los errores se infiltrarán.
Y lo peor de todo: no se puede suponer con seguridad el grado de corrupción (excepto que el client
ahora está fallido). O al menos, no supone que va a soportar el tiempo (y el código cambia).
Como suele suceder, la única forma de ganar es no jugar.
Una posible solución: Transacciones
Siempre que sea posible, la idea clave es definir funciones macro , que fallarán o producirán el resultado esperado. Esas son nuestras transacciones . Y su forma es invariante:
Either<Output, Error> doit(Input const&);
// or
Output doit(Input const&); // throw in case of error
Una transacción no modifica ningún estado externo, por lo tanto, si no produce un resultado:
- el mundo externo no ha cambiado (nada para deshacer)
- no hay un resultado parcial para observar
Se debe considerar que cualquier función que no sea una transacción corrompió todo lo que tocó y, por lo tanto, la única forma razonable de lidiar con un error de las funciones no transaccionales es dejarla burbujear hasta que se alcance una capa de transacción. Cualquier intento de resolver el error antes está, al final, condenado al fracaso.
¿Cómo decidir si un error debe manejarse localmente o propagarse a un código de nivel superior?
En caso de excepciones, ¿dónde debería generalmente atraparlos? En el código de nivel bajo o superior?
Trate con ellos siempre que sea seguro hacerlo y hay un valor al hacerlo. En particular, está bien detectar un error, verificar si se puede tratar localmente y luego tratarlo o pasarlo.
Debería esforzarse por una estrategia unificada de manejo de errores a través de todas las capas de código, o tratar de desarrollar un sistema que pueda adaptarse a una variedad de estrategias de manejo de errores (para poder manejar los errores de bibliotecas de terceros).
No me referí a esta pregunta anteriormente, sin embargo, creo que está claro que el enfoque que destaqué ya es doble, ya que consta de ambos tipos de resultados y excepciones. Como tal, tratar con bibliotecas de terceros debería ser fácil, aunque sí recomiendo que las envuelva de todos modos por otros motivos (el código de un tercero está mejor aislado que una interfaz orientada a los negocios que tenga la adaptación de impedancia).
¿Cómo decidir si un error debe manejarse localmente o propagarse a un código de nivel superior?
El manejo de errores debe hacerse al nivel más alto afectado. Si solo afecta el código de nivel inferior, entonces debe manejarse allí. Si el error afecta el código de nivel superior, entonces el error debe manejarse en el nivel superior. Esto es para evitar que algún código de nivel superior continúe feliz después de que un error haya hecho que sus acciones sean incorrectas. Debe saber qué está pasando, siempre que se vea afectado.
¿Cómo decidir entre registrar un error o mostrarlo como un mensaje de error para el usuario?
Siempre debe registrar el error. Debería mostrar el error al usuario cuando se vean afectados por él. Si es algo que nunca notarán y no tiene un impacto directo (p. Ej., No se pudieron abrir dos tomas antes de que finalmente se abriera el tercero, lo que provocó una demora muy breve para que el usuario no se informara), no deberían notificarse.
¿Está registrando algo que solo debería hacerse en el código de la aplicación? ¿O está bien hacer algo de registro desde el código de la biblioteca?
Demasiada tala rara vez es algo malo. Te arrepentirás de no haber iniciado sesión cuando tienes que buscar un error de la biblioteca más de lo que te frustrarán los registros extra cuando busque otros errores.
En caso de excepciones, ¿dónde debería generalmente atraparlos? En el código de nivel bajo o superior?
De manera similar al manejo de errores anterior, debe capturarse donde está el impacto, y donde el error puede ser corregido / manejado efectivamente. Esto variará de un caso a otro.
Debería esforzarse por una estrategia unificada de manejo de errores a través de todas las capas de código, o tratar de desarrollar un sistema que pueda adaptarse a una variedad de estrategias de manejo de errores (para poder manejar los errores de bibliotecas de terceros).
Esta es en gran medida una decisión personal. Mi manejo interno de errores es muy diferente al manejo de errores que uso para cualquier cosa que toque una biblioteca de terceros. Tengo una idea general de qué esperar de mi código, pero las cosas de terceros podrían pasarle algo.
¿Tiene sentido crear una lista de códigos de error? ¿O está pasado de moda en estos días? Depende de cuánto esperas tener errores lanzados. Puede que te encante la lista de códigos de error si pasas mucho tiempo buscando errores, ya que pueden ayudarte a orientarte en la dirección correcta. Sin embargo, cualquier tiempo dedicado a la construcción de estos es menos tiempo dedicado a la codificación / corrección de errores, por lo que es una bolsa mixta. Esto en gran parte se reduce a preferencia personal.
¿Cómo decidir si un error debe manejarse localmente o propagarse a un código de nivel superior?
Si la excepción interrumpe el funcionamiento de un método, es un buen enfoque para lanzarlo a un nivel superior. Si está familiarizado con MVC, las Excepciones deben evaluarse en el Controlador.
¿Cómo decidir entre registrar un error o mostrarlo como un mensaje de error para el usuario? Los errores de registro y toda la información disponible sobre el error son un buen enfoque. Si el error interrumpe la operación o el usuario necesita saber que se ha producido un error, debe mostrarlo al usuario. Tenga en cuenta que en un servicio de Windows los registros son muy muy importantes.
¿Está registrando algo que solo debería hacerse en el código de la aplicación? ¿O está bien hacer algo de registro desde el código de la biblioteca?
No veo ningún motivo para registrar errores en un dll. Solo debería arrojar errores. Puede haber una razón específica para hacer, por supuesto. En nuestra empresa, un dll registra información sobre el proceso (no solo errores)
En caso de excepciones, ¿dónde debería generalmente atraparlos? En el código de nivel bajo o superior? Pregunta similar: ¿en qué punto debería dejar de propagar un error y tratar con él?
En un controlador.
Editar: Necesito explicar esto un poco si no estás familiarizado con MVC. Model View Controller es un patrón de diseño. En Model, desarrolla la lógica de la aplicación. En View, muestra contenido para el usuario. En el Controlador, obtiene eventos del usuario y llama al Modelo para la función relevante, luego llama a la Vista para mostrar el resultado al usuario.
Supongamos que tiene un formulario que tiene dos cuadros de texto y una etiqueta y un botón llamado Agregar. Como puedes imaginar, esta es tu opinión. El evento Button_Click se define en Controller. Y un método add se define en Model. Cuando el usuario hace clic, se activa el evento Button_Click y las llamadas del controlador agregan el método. Aquí los valores del cuadro de texto pueden estar vacíos o pueden ser letras en lugar de números. Se produce una excepción en la función de agregar y se lanza esta excepción. Controlador lo maneja Y muestra un mensaje de error en la etiqueta.
Debería esforzarse por una estrategia unificada de manejo de errores a través de todas las capas de código, o tratar de desarrollar un sistema que pueda adaptarse a una variedad de estrategias de manejo de errores (para poder manejar los errores de bibliotecas de terceros).
Yo prefiero el segundo. Sería más fácil. Y no creo que puedas hacer cosas generales para el manejo de errores. Especialmente para diferentes bibliotecas.
¿Tiene sentido crear una lista de códigos de error? ¿O está pasado de moda en estos días?
Eso depende de cómo lo usarás. En una sola aplicación (un sitio web, una aplicación de escritorio), no creo que sea necesario. Pero si desarrolla un servicio web, ¿cómo informará a los usuarios sobre errores? Proporcionar un código de error siempre es importante aquí.
If (error.Message == "User Login Failed")
{
//do something.
}
If (error.Code == "102")
{
//do something.
}
¿Cuál prefieres?
Y hay otra forma de códigos de error en estos días:
If (error.Code == "LOGIN_ERROR_102") // wrong password
{
//do something.
}
Los demás pueden ser: LOGIN_ERROR_103 (p. Ej .: este usuario ha caducado), etc.
Este también es legible por humanos.
Aquí hay una impresionante publicación de blog que explica cómo debe hacerse el manejo de errores. http://damienkatz.net/2006/04/error_code_vs_e.html
¿Cómo decidir si un error debe manejarse localmente o propagarse a un código de nivel superior? Al igual que Martin Becket dice en otra respuesta, esta es una cuestión de si el error se puede arreglar aquí o no.
¿Cómo decidir entre registrar un error o mostrarlo como un mensaje de error para el usuario? Probablemente nunca deberías mostrar un error al usuario si piensas así. Más bien, muéstreles un mensaje bien formado que explique la situación, sin dar demasiada información técnica. Luego, registre la información técnica, especialmente si se trata de un error al procesar la entrada. Si su código no sabe cómo manejar una entrada defectuosa, entonces DEBE solucionarse.
¿Está registrando algo que solo debería hacerse en el código de la aplicación? ¿O está bien hacer algo de registro desde el código de la biblioteca? Iniciar sesión en el código de la biblioteca no es útil, porque es posible que ni siquiera lo haya escrito. Sin embargo, la aplicación podría registrar la interacción con el código de la biblioteca e incluso a través de estadísticas detectar errores.
En caso de excepciones, ¿dónde debería generalmente atraparlos? En el código de nivel bajo o superior? Ver pregunta uno.
Pregunta similar: ¿en qué punto debería dejar de propagar un error y tratar con él? Ver pregunta uno.
Debería esforzarse por una estrategia unificada de manejo de errores a través de todas las capas de código, o tratar de desarrollar un sistema que pueda adaptarse a una variedad de estrategias de manejo de errores (para poder manejar los errores de bibliotecas de terceros). Lanzar excepciones es una operación costosa en la mayoría de los lenguajes pesados, así que úselos donde el flujo completo del programa se interrumpe para esa operación. Por otro lado, si puede predecir todos los resultados de una función, coloque cualquier dato a través de una variable a la que se hace referencia como parámetro y devuelva un código de error (0 en caso de éxito, 1+ en errores).
¿Tiene sentido crear una lista de códigos de error? ¿O está pasado de moda en estos días? Haga una lista de códigos de error para una función en particular y documente dentro de ella como una lista de posibles valores de retorno. Vea la pregunta anterior y el enlace.