Cómo diseñar excepciones

11

Estoy luchando con una pregunta muy simple:

Ahora estoy trabajando en una aplicación de servidor, y necesito inventar una jerarquía para las excepciones (algunas excepciones ya existen, pero se necesita un marco general). ¿Cómo empiezo a hacer esto?

Estoy pensando en seguir esta estrategia:

1) ¿Qué va mal?

  • Se pregunta algo, que no está permitido.
  • Se pregunta algo, está permitido, pero no funciona debido a parámetros incorrectos.
  • Se pregunta algo, está permitido, pero no funciona debido a errores internos.

2) ¿Quién está lanzando la solicitud?

  • La aplicación cliente
  • Otra aplicación de servidor

3) Entrega de mensajes: como se trata de una aplicación de servidor, se trata de recibir y enviar mensajes. ¿Y qué pasa si el envío de un mensaje sale mal?

Como tal, podríamos obtener los siguientes tipos de excepción:

  • ServerNotAllowedException
  • ClientNotAllowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException (en caso de que el servidor no sepa de dónde proviene la solicitud)
    • ServerInternalException
    • ClientInternalException
  • MessageHandlingException

Este es un enfoque muy general para definir la jerarquía de excepciones, pero me temo que me pueden faltar algunos casos obvios. ¿Tiene ideas sobre qué áreas no estoy cubriendo, conoce algún inconveniente de este método o hay un enfoque más general para este tipo de preguntas (en el último caso, ¿dónde puedo encontrarlo?)

Gracias por adelantado

Dominique
fuente
55
No mencionó lo que desea lograr con su jerarquía de clases de excepción (y eso no es nada obvio). ¿Registro significativo? ¿Permitir que los clientes reaccionen razonablemente a las diferentes excepciones? ¿O que?
Ralf Kleberhoff
2
Probablemente sea útil analizar algunas historias y usar casos primero, y ver qué sale. Por ejemplo: el cliente solicita X , no está permitido. El cliente solicita X , pero la solicitud no es válida. Analícelos pensando en quién debe manejar la excepción, qué pueden hacer con ella (aviso, reintento, lo que sea) y qué información necesitan para hacerlo bien. Luego , una vez que sepa cuáles son sus excepciones concretas y qué información necesitan los manejadores para procesarlas, puede formarlas en una agradable jerarquía.
Inútil el
1
Nunca he entendido realmente el deseo de querer usar tantos tipos de excepciones diferentes. Por lo general, para la mayoría de los catchbloques que uso, no tengo mucho más uso para la excepción que el mensaje de error que contiene. Realmente no tengo nada diferente que pueda hacer por una excepción involucrada en no leer un archivo, ya que no se puede asignar memoria durante el proceso de lectura, por lo que tiendo a captar std::exceptione informar el mensaje de error que contiene, tal vez decorar con "Failed to open file: %s", ex.what()un buffer de pila antes de imprimirlo.
3
Además, no puedo anticipar todos los tipos de excepción que se lanzarán en primer lugar. Es posible que pueda anticiparlos ahora, pero los colegas pueden presentar otros nuevos en el futuro, por ejemplo, es un poco inútil para mí saber, por ahora y para siempre, todos los diferentes tipos de excepciones que podrían generarse en el proceso de un operación. Así que solo atrapo súper generalmente con un solo bloque de captura. He visto ejemplos de personas que usan muchos catchbloques diferentes en un solo sitio de recuperación, pero a menudo es solo ignorar el mensaje dentro de la excepción e imprimir un mensaje más localizado ...

Respuestas:

5

Observaciones generales

(un poco sesgado de opinión)

Por lo general, no elegiría una jerarquía de excepción detallada.

Lo más importante: una excepción le dice a su interlocutor que su método no pudo completar su trabajo. Y su interlocutor debe recibir un aviso al respecto, para que no simplemente continúe. Eso funciona con cualquier excepción, sin importar la clase de excepción que elija.

El segundo aspecto es el registro. Desea encontrar entradas de registro significativas cuando algo sale mal. Eso tampoco necesita diferentes clases de excepción, solo mensajes de texto bien diseñados (supongo que no necesita un autómata para leer sus registros de errores ...).

El tercer aspecto es la reacción de su interlocutor. ¿Qué puede hacer su interlocutor cuando recibe una excepción? Aquí puede tener sentido tener diferentes clases de excepción, por lo que la persona que llama puede decidir si volver a intentar la misma llamada, usar una solución diferente (por ejemplo, usar una fuente alternativa) o darse por vencido.

Y tal vez desee utilizar sus excepciones como base para informar al usuario final sobre el problema. Eso significa crear un mensaje fácil de usar además del texto de administración para el archivo de registro, pero no necesita diferentes clases de excepción (aunque tal vez eso pueda facilitar la generación de texto ...).

Un aspecto importante para el registro (y para los mensajes de error del usuario) es la capacidad de enmendar la excepción con información de contexto al capturarla en alguna capa, agregar cierta información de contexto, por ejemplo, parámetros del método, y volver a lanzarla.

Su jerarquía

¿Quién está lanzando la solicitud? No creo que necesite la información sobre quién inició la solicitud. Ni siquiera puedo imaginar cómo sabes eso en el fondo de una pila de llamadas.

Manejo de mensajes : ese no es un aspecto diferente, sino solo casos adicionales para "¿Qué va mal?".

En un comentario, habla de un indicador de " no iniciar sesión " al crear una excepción. No creo que en el lugar donde cree y arroje una excepción, pueda tomar una decisión confiable sobre si registrar o no esa excepción.

La única situación que puedo imaginar es que alguna capa superior usa su API de una manera que a veces producirá excepciones, y esta capa sabe que no necesita molestar a ningún administrador con la excepción, por lo que se traga la excepción en silencio. Pero ese es un olor a código: una excepción esperada es una contradicción en sí misma, una pista para cambiar la API. Y es la capa superior la que debería decidir, no el código generador de excepciones.

Ralf Kleberhoff
fuente
En mi experiencia, un código de error en combinación con un texto fácil de usar funciona muy bien. Los administradores pueden usar el código de error para encontrar información adicional.
Sjoerd
1
Me gusta la idea detrás de esta respuesta en general. Desde mi experiencia, las excepciones realmente nunca deberían convertirse en una bestia demasiado compleja. El objetivo principal de una excepción es permitir que el código de llamada aborde un problema específico y obtenga la información relevante de depuración / reintento sin alterar la respuesta de la función.
greggle138
2

Lo principal a tener en cuenta al diseñar un patrón de respuesta de error es asegurarse de que sea útil para las personas que llaman. Esto se aplica si está utilizando excepciones o códigos de error definidos, pero nos limitaremos a ilustrar con excepciones.

  • Si su lenguaje o marco ya proporciona clases de excepción general, úselas donde sean apropiadas y donde sea razonable esperarlas . No defina sus propias clases ArgumentNullExceptiono las ArgumentOutOfRangeclases de excepción. Las personas que llaman no esperarán atraparlos.

  • Defina una MyClientServerAppExceptionclase base para abarcar los errores que son únicos dentro del contexto de su aplicación. Nunca arrojes una instancia de la clase base. Una respuesta de error ambigua es LO PEOR NUNCA . Si hay un "error interno", explique cuál es ese error.

  • En su mayor parte, la jerarquía debajo de la clase base debe ser amplia, no profunda. Solo necesita profundizarlo en situaciones en las que es útil para la persona que llama. Por ejemplo, si hay 5 razones por las que un mensaje puede fallar del cliente al servidor, puede definir una ServerMessageFaultexcepción y luego definir una clase de excepción para cada uno de esos 5 errores debajo de eso. De esa manera, la persona que llama puede capturar la superclase si lo necesita o quiere. Intente limitar esto a casos específicos y razonables.

  • No intente definir todas sus clases de excepción antes de que realmente se usen. Terminarás volviendo a hacer la mayor parte. Cuando encuentre un caso de error al escribir código, decida cuál es la mejor manera de describir ese error. Idealmente, debería expresarse en el contexto de lo que la persona que llama está tratando de hacer.

  • En relación con el punto anterior, recuerde que solo porque use excepciones para responder a los errores, eso no significa que tenga que usar solo excepciones para los estados de error. Tenga en cuenta que lanzar excepciones es usualmente costoso, y el costo de rendimiento puede variar de un idioma a otro. Con algunos idiomas, el costo es mayor dependiendo de la profundidad de la pila de llamadas, por lo que si un error es profundo dentro de una pila de llamadas, verifique si no puede usar tipos primitivos simples (códigos de error enteros o banderas booleanas) para empujar el error hace una copia de seguridad de la pila, por lo que puede arrojarse más cerca de la invocación de la persona que llama.

  • Si incluye el registro como parte de la respuesta de error, debería ser fácil para las personas que llaman agregar información de contexto a un objeto de excepción. Comience desde donde se usa la información en el código de registro. Decida cuánta información se necesita para que el registro sea útil (sin ser un muro gigante de texto). Luego trabaje hacia atrás para asegurarse de que las clases de excepción puedan proporcionarse fácilmente con esa información.

Por último, a menos que su aplicación puede absolutamente lidiar con gracia con un error fuera de la memoria, no se trata de hacer frente a esos, u otras excepciones de tiempo de ejecución catastróficos. Simplemente deje que el sistema operativo lo solucione, porque en realidad, eso es todo lo que puede hacer.

Mark Benningfield
fuente
2
A excepción de su penúltima viñeta (sobre el costo de las excepciones), la respuesta es buena. Esa viñeta sobre el costo de las excepciones es engañosa, porque en todas las formas comunes de implementar excepciones en el nivel bajo, el costo de lanzar una excepción es completamente independiente de la profundidad de la pila de llamadas. Los métodos alternativos de informe de errores podrían ser mejores si sabe que el error será manejado por la persona que llama inmediatamente, pero no para obtener algunas funciones de la pila de llamadas antes de lanzar una excepción.
Bart van Ingen Schenau
@BartvanIngenSchenau: Traté de no vincular esto a un idioma específico. En algunos lenguajes ( Java, por ejemplo ), la profundidad de la pila de llamadas afecta el costo de la creación de instancias. Lo editaré para reflejar que no está tan cortado y seco.
Mark Benningfield
0

Bueno, le recomendaría que, ante todo, cree una Exceptionclase base para todas las excepciones marcadas que su aplicación pueda generar . Si se llamó a su aplicación DuckType, haga una DuckTypeExceptionclase base.

Esto le permite detectar cualquier excepción de su DuckTypeExceptionclase base para el manejo. A partir de aquí, sus excepciones deben ramificarse con nombres descriptivos que puedan resaltar mejor el tipo de problema. Por ejemplo, "DatabaseConnectionException".

Permítanme ser claro, todas estas excepciones deben verificarse para situaciones que podrían ocurrir y que probablemente desearían manejar con gracia en su programa. En otras palabras, no puede conectarse a la base de datos, por lo que DatabaseConnectionExceptionse genera un mensaje que luego puede atrapar para esperar y volver a intentar después de un período de tiempo.

Se podría no ver una excepción comprobada para un problema muy inesperado, como una consulta SQL válida o una excepción de puntero nulo, y les animo a dejar que estas excepciones trascienden la mayoría de las cláusulas de captura (o capturados y relanza como sea necesario) hasta que llegue a la principal controlador en sí mismo, que luego puede capturarlo RuntimeExceptionúnicamente con fines de registro.

Mi preferencia personal es no volver a lanzar una RuntimeExceptionexcepción no verificada como otra excepción, ya que por la naturaleza de una excepción no verificada, no lo esperaría y, por lo tanto, volver a lanzarla bajo otra excepción es ocultar información. Sin embargo, si esa es su preferencia, aún puede atrapar RuntimeExceptiony lanzar un DuckTypeInternalExceptionque, a diferencia de lo que se DuckTypeExceptionderiva, no RuntimeExceptionestá marcado.

Si lo prefiere, puede subcategorizar sus excepciones para fines organizativos, como DatabaseExceptioncualquier cosa relacionada con la base de datos, pero aún así recomendaría que dichas sub-excepciones se deriven de su excepción base DuckTypeExceptiony que sean abstractas y, por lo tanto, derivadas con nombres descriptivos explícitos.

Como regla general, sus capturas de prueba deberían ser cada vez más genéricas a medida que avanza en la pila de llamadas de los llamantes para manejar excepciones, y nuevamente, en su controlador principal, no manejaría un, DatabaseConnectionExceptionsino el simple DuckTypeExceptionde que derivan todas sus excepciones marcadas.

Neil
fuente
2
Tenga en cuenta que la pregunta está etiquetada "C ++".
Martin Ba
0

Intenta simplificar eso.

Lo primero que lo ayudará a pensar en otra estrategia es: detectar muchas excepciones es muy similar al uso de excepciones comprobadas de Java (lo siento, no soy desarrollador de C ++). Esto no es bueno por muchas razones, así que siempre trato de no usarlas, y su estrategia de excepción de jerarquía me recuerda mucho de eso.

Por lo tanto, le recomiendo otra estrategia flexible: use excepciones no verificadas y errores de código.

Por ejemplo, vea este código Java:

public class SystemErrorCode implements ErrorCode {

    INVALID_NAME(101),
    ORDER_NOT_FOUND(102),
    PARAMETER_NOT_FOUND(103),
    VALUE_TOO_SHORT(104);

    private final int number;

    private ErrorCode(int number) {
        this.number = number;
    }

    @Override
    public int getNumber() {
        return number;
    }
}

Y tu única excepción:

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

Esta estrategia la encontré en este enlace y puede encontrar la implementación de Java aquí , donde puede ver más detalles al respecto, porque el código anterior está simplificado.

Como necesita separar las diferentes excepciones entre las aplicaciones "cliente" y "otro servidor", puede tener múltiples clases de códigos de error que implementen la interfaz ErrorCode.

Dherik
fuente
2
Tenga en cuenta que la pregunta está etiquetada "C ++".
Sjoerd el
Edité para adaptar la idea.
Dherik
0

Las excepciones son gotos sin restricciones y deben usarse con cuidado. La mejor estrategia para ellos es restringirlos. La función de llamada debe manejar todas las excepciones lanzadas por las funciones que llama o el programa muere. Solo la función de llamada tiene el contexto correcto para manejar la excepción. Tener funciones más arriba en el árbol de llamadas que las maneja son gotos sin restricciones.

Las excepciones no son errores. Ocurren cuando las circunstancias impiden que el programa complete una rama del código e indican otra rama para que siga.

Las excepciones deben estar en el contexto de la función llamada. Por ejemplo: una función que resuelve ecuaciones cuadráticas . Debe manejar dos excepciones: division_by_zero y square_root_of_negative_number. Pero estas excepciones no tienen sentido para las funciones que llaman a este solucionador de ecuaciones cuadráticas. Suceden debido al método utilizado para resolver ecuaciones y simplemente volver a lanzarlas expone las partes internas y rompe la encapsulación. En su lugar, division_by_zero se debe volver a generar como not_quadratic y square_root_of_negative_number y no_real_roots.

No hay necesidad de una jerarquía de excepciones. Una enumeración de las excepciones (que las identifica) lanzadas por una función es suficiente porque la función de llamada debe manejarlas. Permitirles procesar el árbol de llamadas es un goto fuera de contexto (sin restricciones).

Shawnhcorey
fuente