¿Cuál es un 'buen número' de excepciones para implementar en mi biblioteca?

20

Siempre me he preguntado cuántas clases de excepción diferentes debería implementar y lanzar para varias piezas de mi software. Mi desarrollo particular suele estar relacionado con C ++ / C # / Java, pero creo que esta es una pregunta para todos los lenguajes.

Quiero comprender qué es un buen número de excepciones diferentes para lanzar, y qué espera la comunidad de desarrolladores de una buena biblioteca.

Las compensaciones que veo incluyen:

  • Más clases de excepción pueden permitir niveles muy finos de manejo de errores para usuarios de API (propensos a errores de configuración o datos de usuario, o archivos no encontrados)
  • Más clases de excepción permiten incrustar información específica de error en la excepción, en lugar de solo un mensaje de cadena o código de error
  • Más clases de excepción pueden significar más mantenimiento de código
  • Más clases de excepción pueden significar que la API es menos accesible para los usuarios

Los escenarios en los que deseo comprender el uso de excepciones incluyen:

  • Durante la etapa de 'configuración', que puede incluir cargar archivos o configurar parámetros
  • Durante una fase de tipo 'operación' donde la biblioteca podría estar ejecutando tareas y haciendo algún trabajo, quizás en otro hilo

Otros patrones de informe de errores sin usar excepciones, o menos excepciones (en comparación) pueden incluir:

  • Menos excepciones, pero incrustando un código de error que puede usarse como una búsqueda
  • Devolver códigos de error y marcas directamente desde funciones (a veces no es posible desde subprocesos)
  • Implementado un evento o sistema de devolución de llamada en caso de error (evita el desbobinado de la pila)

Como desarrolladores, ¿qué prefieres ver?

Si hay MUCHAS excepciones, ¿se molesta en manejarlas por separado?

¿Tiene preferencia por los tipos de manejo de errores dependiendo de la etapa de operación?

Pelusa
fuente
55
"la API es menos accesible para los usuarios": se puede solucionar con varias clases de excepción que todas heredan de una clase base común para la API. Luego, al comenzar, puede ver de qué API proviene la excepción sin tener que preocuparse por todos los detalles. Para cuando complete su primer programa utilizando la API, si necesita más información sobre el error, entonces comience a ver qué significan realmente las diferentes excepciones. Por supuesto, simultáneamente debe jugar bien con la jerarquía de excepción estándar del idioma que está utilizando.
Steve Jessop el
punto pertinente Steve
Fuzz

Respuestas:

18

Lo mantengo simple.

Una biblioteca tiene un tipo de excepción base extendido desde std ::: runtime_error (que es de C ++ se aplica según corresponda a otros lenguajes). Esta excepción toma una cadena de mensaje para que podamos iniciar sesión; cada punto de lanzamiento tiene un mensaje único (generalmente con una ID única).

Eso es todo.

Nota 1 : En las situaciones en las que alguien que atrapa la excepción puede arreglar las excepciones y reiniciar la acción. Agregaré excepciones derivadas para cosas que se pueden arreglar de manera única y única en una ubicación remota. Pero esto es muy muy raro (recuerde que es poco probable que el receptor esté cerca del punto de lanzamiento, por lo que solucionar el problema será difícil (pero todo depende de la situación)).

Nota 2 : a veces la biblioteca es tan simple que no vale la pena darle su propia excepción y std :: runtime_error funcionará. Solo es importante tener una excepción si la capacidad de distinguirlo de std :: runtime_error puede proporcionar al usuario suficiente información para hacer algo con él.

Nota 3 : Dentro de una clase, generalmente prefiero códigos de error (pero estos nunca escaparán a través de la API pública de mi clase).

Mirando sus compensaciones:

Las compensaciones que veo incluyen:

Más clases de excepción pueden permitir niveles muy finos de manejo de errores para usuarios de API (propensos a errores de configuración o datos de usuario, o archivos no encontrados)

¿Más excepciones realmente te dan un control de grano más fino? La pregunta es si el código de captura realmente puede corregir el error en función de la excepción. Estoy seguro de que hay situaciones como esa y en estos casos debería tener otra excepción. Pero todas las excepciones que ha enumerado anteriormente, la única corrección útil es generar una gran advertencia y detener la aplicación.

Más clases de excepción permiten incrustar información específica de error en la excepción, en lugar de solo un mensaje de cadena o código de error

Esta es una gran razón para usar excepciones. Pero la información debe ser útil para la persona que la está almacenando en caché. ¿Pueden usar la información para realizar alguna acción correctiva? Si el objeto es interno a su biblioteca y no se puede utilizar para influir en ninguna de las API, entonces la información es inútil. Debes ser muy específico para que la información que se arroje tenga un valor útil para la persona que puede atraparla. La persona que lo atrapa generalmente está fuera de su API pública, por lo tanto, adapte su información para que pueda usarse con cosas en su API pública.

Si todo lo que pueden hacer es registrar la excepción, entonces es mejor simplemente lanzar un mensaje de error en lugar de muchos datos. Como el receptor generalmente generará un mensaje de error con los datos. Si crea el mensaje de error, entonces será coherente en todos los receptores, si permite que el receptor construya el mensaje de error, podría obtener el mismo error informado de manera diferente dependiendo de quién llama y captura.

Menos excepciones, pero incrustando un código de error que puede usarse como una búsqueda

Debe determinar el clima, el código de error se puede utilizar de manera significativa. Si puede, entonces debería tener su propia excepción. De lo contrario, sus usuarios ahora deben implementar sentencias de cambio dentro de allí catch (que anula el punto de tener que catch maneje automáticamente las cosas).

Si no puede, ¿por qué no utilizar un mensaje de error en la excepción (no es necesario dividir el código y el mensaje hace que sea difícil buscarlo).

Devolver códigos de error y marcas directamente desde funciones (a veces no es posible desde subprocesos)

Devolver códigos de error es excelente internamente. Le permite corregir errores allí y luego, y debe asegurarse de corregir todos los códigos de error y tenerlos en cuenta. Pero filtrarlos en su API pública es una mala idea. El problema es que los programadores a menudo se olvidan de verificar los estados de error (al menos con una excepción, un error no verificado forzará a la aplicación a cerrar un error no manejado y, por lo general, corromperá todos sus datos).

Implementado un evento o sistema de devolución de llamada en caso de error (evita el desbobinado de la pila)

Este método a menudo se usa junto con otro mecanismo de manejo de errores (no como alternativa). Piensa en tu programa de Windows. Un usuario inicia una acción seleccionando un elemento del menú. Esto genera una acción en la cola de eventos. La cola de eventos eventualmente asigna un hilo para manejar la acción. Se supone que el subproceso maneja la acción y, finalmente, regresa al grupo de subprocesos y espera otra tarea. Aquí una excepción debe ser atrapada en la base por el hilo encargado del trabajo. El resultado de detectar la excepción generalmente generará un evento para el bucle principal que eventualmente generará un mensaje de error para el usuario.

Pero a menos que pueda continuar ante la excepción, la pila se va a desenrollar (al menos para el hilo).

Martin York
fuente
+1; Aunque "De lo contrario, sus usuarios ahora necesitan implementar sentencias de cambio dentro de allí catch (lo que anula el punto de tener que catch maneje automáticamente las cosas)". - El desbobinado de la pila de llamadas (si aún puede usarlo) y el manejo forzado de errores siguen siendo grandes beneficios. Pero sin duda va a restar valor a la capacidad de desenrollar la pila de llamadas cuando tienes que atraparla en el medio y volver a lanzarla.
Merlyn Morgan-Graham
9

Generalmente comienzo con:

  1. Una clase de excepción para errores de argumento . Por ejemplo, "argumento no permitido ser nulo", "argumento debe ser positivo", y así sucesivamente. Java y C # tienen clases predefinidas para aquellos; en C ++, generalmente creo solo una clase, derivada de std :: exception.
  2. Una clase de excepción para errores de precondición . Esos son para pruebas más complejas, como "el índice debe ser más pequeño que el tamaño".
  3. Una clase de excepción para errores de aserción . Esos son para verificar el estado de los métodos intermedios de consistencia. Por ejemplo, al iterar sobre una lista para contar elementos que son negativos, cero o positivos, al final esos tres deberían sumar el tamaño.
  4. Una clase de excepción base para la biblioteca misma. Al principio, solo lanza esta clase. Solo cuando surja la necesidad, comience a agregar subclases.
  5. Prefiero no incluir excepciones, pero sé que las opiniones difieren en este punto (y está muy correlacionado con el lenguaje utilizado). Si lo hace, necesitará clases de excepción de ajuste adicionales .

Como las clases para los primeros 3 casos son ayudas de depuración, no están destinadas a ser manejadas por el código. En cambio, solo deben ser capturados por un controlador de nivel superior que muestre la información de manera que el usuario pueda copiarla y pegarla en el desarrollador (o incluso mejor: presione el botón "enviar informe"). Por lo tanto, incluya información que sea útil para el desarrollador: archivo, función, número de línea y algún mensaje que identifique claramente qué verificación falló.

Como los primeros 3 casos son los mismos para cada proyecto, en C ++ generalmente solo los copio del proyecto anterior. Debido a que muchos están haciendo exactamente lo mismo, los diseñadores de C # y Java agregaron clases estándar para esos casos a la biblioteca estándar. [ACTUALIZACIÓN:] Para los programadores perezosos: una clase podría ser suficiente y, con un poco de suerte, su biblioteca estándar ya tiene una clase de excepción adecuada. Prefiero agregar información como nombre de archivo y número de lino, que las clases predeterminadas en C ++ no proporcionan. [Fin de actualización]

Dependiendo de la biblioteca, el cuarto caso podría tener solo una clase, o podría convertirse en un puñado de clases. Prefiero el enfoque ágil para comenzar simple, agregando subclases cuando surja la necesidad.

Para una argumentación detallada sobre mi cuarto caso, vea la respuesta de Loki Astari . Estoy totalmente de acuerdo con su respuesta detallada.

Sjoerd
fuente
+1; Hay una diferencia definitiva entre cómo debe escribir excepciones de escritura en un marco y cómo debe escribir excepciones en una aplicación. La distinción es un poco confusa, pero lo mencionas (en referencia a Loki para el caso de la biblioteca).
Merlyn Morgan-Graham