Probar / Catch / Log / Rethrow - ¿Es Anti Pattern?

7

Puedo ver varias publicaciones donde se enfatizó la importancia de manejar las excepciones en la ubicación central o en el límite del proceso como una buena práctica, en lugar de ensuciar cada bloque de código alrededor de intentar / atrapar. Creo firmemente que la mayoría de nosotros entendemos la importancia de esto, sin embargo, veo que las personas aún están terminando con el patrón anti de captura de registro y retorno, principalmente porque para facilitar la resolución de problemas durante cualquier excepción, quieren registrar más información específica del contexto (ejemplo: parámetros del método pasado) y la forma es envolver el método alrededor de try / catch / log / rethrow.

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        logger.log("error occured while number 1 = {num1} and number 2 = {num2}"); 
        throw;
    }
}

¿Hay alguna manera correcta de lograr esto mientras se mantiene la buena práctica de manejo de excepciones? Escuché sobre el marco de AOP como PostSharp para esto, pero me gustaría saber si existe algún costo de rendimiento importante o negativo asociado con estos marcos de AOP.

¡Gracias!

    
pregunta Rahul Agarwal 06.02.2018 - 11:43

6 respuestas

7

El problema no es el bloque catch local, el problema es registrar y volver a generar . Maneja la excepción o envuélvela con una nueva excepción que agregue contexto adicional y lance eso. De lo contrario, se encontrará con varias entradas de registro duplicadas para la misma excepción.

La idea aquí es mejorar la capacidad de depurar tu aplicación.

Ejemplo # 1: manejarlo

try
{
    doSomething();
}
catch (Exception e)
{
    log.Info("Couldn't do something", e);
    doSomethingElse();
}

Si maneja la excepción, puede degradar fácilmente la importancia de la entrada del registro de excepciones y no hay razón para infiltrar esa excepción en la cadena. Ya se ha tratado.

El manejo de una excepción puede incluir informar a los usuarios que ocurrió un problema, registrar el evento o simplemente ignorarlo.

NOTA: si ignora intencionalmente una excepción, recomiendo proporcionar un comentario en la cláusula de captura vacía que indique claramente por qué. Esto permite que los futuros mantenedores sepan que no fue un error o una programación perezosa. Ejemplo:

try
{
    context.DrawLine(x1,y1, x2,y2);
}
catch (OutOfMemoryException)
{
    // WinForms throws OutOfMemory if the figure you are attempting to
    // draw takes up less than one pixel (true story)
}

Ejemplo # 2: agregar contexto adicional y lanzar

try
{
    doSomething(line);
}
catch (Exception e)
{
    throw new MyApplicationException(filename, line, e);
}

Agregar contexto adicional (como el número de línea y el nombre del archivo en el código de análisis) puede ayudar a mejorar la capacidad de depurar los archivos de entrada, asumiendo que el problema está ahí. Este es un tipo de caso especial, por lo que volver a envolver una excepción en una "excepción de aplicación" solo para cambiar la marca no ayuda a depurar. Asegúrate de agregar información adicional.

Ejemplo # 3: No hagas nada con la excepción

try
{
    doSomething();
}
finally
{
   // cleanup resources but let the exception percolate
}

En este último caso, solo permites que la excepción se vaya sin tocarla. El controlador de excepciones en la capa más externa puede manejar el registro. La cláusula finally se usa para asegurarse de que se limpien todos los recursos que necesita su método, pero este no es el lugar donde se registra la excepción.

    
respondido por el Berin Loritsch 06.02.2018 - 19:56
5

No creo que las capturas locales sean un anti-patrón, de hecho, si recuerdo correctamente, ¡en realidad se aplica en Java!

Lo que es clave para mí cuando implemento el manejo de errores es la estrategia general. Es posible que desee un filtro que detecte todas las excepciones en el límite del servicio, que quiera interceptarlas manualmente; ambas están bien siempre que exista una estrategia general que se ajuste a los estándares de codificación de sus equipos.

Personalmente, me gusta detectar errores dentro de una función cuando puedo realizar una de las siguientes acciones:

  • Agregar información contextual (como el estado de los objetos o lo que estaba pasando)
  • Maneja la excepción de manera segura (como un método TryX)
  • Su sistema está cruzando un límite de servicio y llamando a una biblioteca o API externa
  • Desea capturar y volver a generar un tipo diferente de excepción (quizás con el original como una excepción interna)
  • La excepción se lanzó como parte de alguna funcionalidad de fondo de bajo valor

Si no es uno de estos casos, no agrego un try / catch local. Si es así, dependiendo del escenario, puedo manejar la excepción (por ejemplo, un método TryX que devuelve un falso) o volver a realizarla, por lo que la estrategia global manejará la excepción.

Por ejemplo:

public bool TryConnectToDatabase()
{
  try
  {
    this.ConnectToDatabase(_databaseType); // this method will throw if it fails to connect
    return true;
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    return false;
  }
}

O un ejemplo de reedición:

public IDbConnection ConnectToDatabase()
{
  try
  {
    // connect to the database and return the connection, will throw if the connection cannot be made
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    throw;
  }
}

Luego atrapas el error en la parte superior de la pila y presentas un mensaje agradable para el usuario.

Sea cual sea el enfoque que tome, siempre vale la pena crear pruebas unitarias para estos escenarios, de modo que pueda asegurarse de que la funcionalidad no cambie e interrumpa el flujo del proyecto en una fecha posterior.

No has mencionado en qué idioma estás trabajando pero eres un desarrollador de .NET y lo has visto demasiadas veces para no mencionarlo.

NO ESCRIBA:

catch(Exception ex)
{
  throw ex;
}

Uso:

catch(Exception ex)
{
  throw;
}

¡El anterior restablece el seguimiento de la pila y hace que tu nivel superior de captura sea absolutamente inútil!

TLDR

La captura local no es un antipatrón, a menudo puede ser parte de un diseño y puede ayudar a agregar un contexto adicional al error.

    
respondido por el Liath 06.02.2018 - 12:23
3

Esto depende mucho del idioma. P.ej. C ++ no ofrece rastreos de pila en los mensajes de error de excepción, por lo que puede ser útil rastrear la excepción a través de frecuentes captura-registro-retorno En contraste, Java y otros lenguajes similares ofrecen muy buenos seguimientos de pila, aunque el formato de estos rastreos de pila puede no ser muy configurable. La captura y el reenvío de excepciones en estos idiomas no tiene ningún sentido a menos que realmente pueda agregar algún contexto importante (por ejemplo, conectar una excepción de SQL de bajo nivel con el contexto de una operación de lógica de negocios).

Cualquier estrategia de manejo de errores que se implementa a través de la reflexión es casi necesariamente menos eficiente que la funcionalidad integrada en el lenguaje. Además, el registro generalizado tiene una sobrecarga de rendimiento inevitable. Por lo tanto, debe equilibrar el flujo de información que obtiene con otros requisitos para este software. Dicho esto, las soluciones como PostSharp que se basan en instrumentación a nivel de compilador generalmente funcionan mejor que la reflexión en tiempo de ejecución.

Personalmente creo que el registro de todo no es útil, ya que incluye mucha información irrelevante. Por lo tanto, soy escéptico de las soluciones automatizadas. Dado un buen marco de registro, podría ser suficiente tener una guía de codificación acordada que discuta qué tipo de información le gustaría registrar y cómo se debe formatear esta información. Luego puede agregar el registro donde sea necesario.

El inicio de sesión en la lógica empresarial es mucho más importante que el inicio de sesión en las funciones de utilidad. Y la recopilación de trazas de informes de fallas del mundo real (que solo requieren el registro en el nivel superior de un proceso) le permite localizar áreas del código donde el registro tendría más valor.

    
respondido por el amon 06.02.2018 - 12:21
2

Cuando veo try/catch/log en todos los métodos, surge la preocupación de que los desarrolladores no tenían idea de lo que podría o no podría pasar en su aplicación, asumieron lo peor y registraron preventivamente todo en todas partes debido a todos los errores que estaban esperando.

Este es un síntoma de que las pruebas de unidad y de integración son insuficientes y los desarrolladores están acostumbrados a pasar por un montón de código en el depurador y esperan que de alguna manera un montón de registro les permita implementar código con errores en un entorno de prueba y encontrar los problemas mirando los registros.

El código que lanza las excepciones puede ser más útil que el código redundante que captura y registra las excepciones. Si lanza una excepción con un mensaje significativo cuando un método recibe un argumento inesperado (y lo registra en el límite del servicio) es mucho más útil que registrar inmediatamente la excepción lanzada como un efecto secundario del argumento no válido y tener que adivinar qué lo causó. .

Los nulos son un ejemplo. Si obtiene un valor como argumento o el resultado de una llamada de método y no debería ser nulo, arroje la excepción. No solo registre el resultado resultante NullReferenceException cinco líneas más tarde debido al valor nulo. De cualquier manera, obtienes una excepción, pero una te dice algo mientras que la otra te hace buscar algo.

Como han dicho otros, es mejor registrar excepciones en el límite del servicio, o siempre que una excepción no se vuelva a eliminar porque se maneja con gracia. La diferencia más importante es entre algo y nada. Si sus excepciones se registran en un lugar al que es fácil llegar, encontrará la información que necesita cuando la necesita.

    
respondido por el Scott Hannen 07.02.2018 - 03:32
1

La excepción en sí misma debe proporcionar toda la información necesaria para el registro adecuado, incluido el mensaje, el código de error y lo que no. Por lo tanto, no debería ser necesario capturar una excepción solo para volver a lanzarla o lanzar una excepción diferente.

A menudo verá el patrón de varias excepciones capturadas y devueltas como una excepción común, como capturar una DatabaseConnectionException, una InvalidQueryException y una InvalidSQLParameterException y volver a enviar una DatabaseException. A pesar de esto, argumentaría que todas estas excepciones específicas deberían derivarse de DatabaseException en primer lugar, por lo que no es necesario volver a generarlas.

Encontrará que eliminar las cláusulas de captura de prueba innecesarias (incluso las que solo tienen un propósito de registro) en realidad facilitará el trabajo, no lo hará más difícil. Solo los lugares en su programa que manejan la excepción deben registrar la excepción y, en caso de que todo lo demás falle, un controlador de excepciones en todo el programa para un último intento de registrar la excepción antes de salir del programa con gracia. La excepción debe tener un seguimiento de pila completo que indique el punto exacto en el que se lanzó la excepción, por lo que a menudo no es necesario proporcionar un registro de "contexto".

Dicho esto, AOP puede ser una solución rápida para usted, aunque generalmente conlleva una leve desaceleración en general. Le alentaría a que elimine las cláusulas de captura de prueba innecesarias por completo donde no se agrega nada de valor.

    
respondido por el Neil 06.02.2018 - 12:24
1

Si necesita registrar información de contexto que aún no está en la excepción, envuélvala en una nueva excepción y proporcione la excepción original como InnerException . De esa manera aún conservas el rastro de pila original. Entonces:

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        throw new Exception("error occured while number 1 = {num1} and number 2 = {num2}", ex);
    }
}

El segundo parámetro del constructor Exception proporciona una excepción interna. Luego, puede registrar todas las excepciones en un solo lugar, y aún obtiene el seguimiento completo de la pila y la información contextual en la misma entrada de registro.

Es posible que desee utilizar una clase de excepción personalizada, pero el punto es el mismo.

try / catch / log / rethrow es un desastre porque puede generar registros confusos, por ejemplo, ¿Qué sucede si ocurre una excepción diferente en otro hilo entre el registro de la información contextual y el registro de la excepción real en el controlador de nivel superior? Sin embargo, intentar / atrapar / lanzar está bien si la nueva excepción agrega información al original.

    
respondido por el JacquesB 07.02.2018 - 08:59

Lea otras preguntas en las etiquetas