2010-06-30 8 views
11

En serio, ¿cómo puede manejar todas esas excepciones sin volverse loco? ¿He leído demasiados artículos sobre el manejo de excepciones o qué? I intenté refactorizar esto un par de veces y cada vez parece que termino con algo aún peor. Tal vez debería admitir que las excepciones suceden y simplemente disfrutar de la codificación de la ruta feliz feliz? ;) Entonces, ¿qué hay de malo con este código (además del hecho de que era lo suficientemente flojo como para lanzar Exception en lugar de algo más específico)? Y, por supuesto, no seas fácil conmigo.Cuando tantas cosas pueden salir mal, todo lo que hace es intentar, intentar, probar

public void Export(Database dstDb) 
{ 
    try 
    { 
     using (DbConnection connection = dstDb.CreateConnection()) 
     { 
      connection.Open(); 
      DbTransaction transaction = connection.BeginTransaction(); 
      try 
      { 
       // Export all data here (insert into dstDb) 
       transaction.Commit(); 
      } 
      catch (SqlException sqlex) 
      { 
       ExceptionHelper.LogException(sqlex); 
       try 
       { 
        transaction.Rollback(); 
       } 
       catch (Exception rollbackEx) 
       { 
        logger.Error("An exception of type " + rollbackEx.GetType() + 
             " was encountered while attempting to roll back the transaction."); 
       } 
       throw new Exception("Error exporting message " + Type + " #" + Id + ": [" + sqlex.GetType() + "] " + sqlex.Message, sqlex); 
      } 
      catch (Exception ex) 
      { 
       try 
       { 
        transaction.Rollback(); 
       } 
       catch (Exception rollbackEx) 
       { 
        logger.Error("An exception of type " + rollbackEx.GetType() + 
             " was encountered while attempting to roll back the transaction."); 
       } 
       throw new Exception("Error exporting message " + Type + " #" + Id + ": [" + ex.GetType() + "] " + ex.Message, ex); 
      } 
     } 

     try 
     { 
      Status = MessageStatus.FINISHED; 
      srcDb.UpdateDataSet(drHeader.Table.DataSet, HEADERS, 
       CreateHeaderInsertCommand(), CreateHeaderUpdateCommand(), null); 
     } 
     catch (Exception statusEx) 
     { 
      logger.ErrorException("Failed to change message status to FINISHED: " + 
            Type + " #" + Id + ": " + statusEx.Message, statusEx); 
     } 
    } 
    catch (Exception importEx) 
    { 
     try 
     { 
      Status = MessageStatus.ERROR; 
      srcDb.UpdateDataSet(drHeader.Table.DataSet, HEADERS, 
        CreateHeaderInsertCommand(), CreateHeaderUpdateCommand(), null); 
     } 
     catch (Exception statusEx) 
     { 
      logger.ErrorException("Failed to change message status to ERROR: " + 
            Type + " #" + Id + ": " + statusEx.Message, statusEx); 
     } 
     AddErrorDescription(importEx.Message); 
     throw new Exception("Couldn't export message " + Type + " #" + Id + ", exception: " + importEx.Message, importEx); 
    } 
} 

Btw. Tantas veces intenté realmente ser lo más específico posible al formular preguntas; el resultado fue que no hubo visitas, ni respuestas, ni idea de cómo resolver el problema. Esta vez pensé en todas las ocasiones en las de otra persona pregunta me llamó la atención, supongo que esto era lo correcto a hacer :)

Actualización:

He intentado poner algunos de los consejos en práctica y esto es lo que se me ocurrió hasta ahora. Decidí modificar ligeramente el comportamiento: cuando no es posible establecer el estado del mensaje en FINISHED después de la exportación exitosa, lo considero como un trabajo que no se ha realizado del todo y restituyo y lanzo una excepción. Si todavía les queda algo de paciencia, háganme saber si es mejor. O arroja algunas críticas más a mí. Por cierto. Gracias por todas las respuestas, analicé cada una de ellas.

Lanzar una instancia de System.Exception no me pareció correcto, así que me deshice de eso, como se sugirió, y en su lugar decidí introducir una excepción personalizada. Esto, por cierto, tampoco parece correcto, ¿exagerado? Esto parece estar bien con los métodos públicos, pero un poco sobre-diseñado para un miembro privado, y aún así quiero saber que hubo un problema con el cambio de estado del mensaje en lugar de un problema con la conexión a la base de datos o algo así.

puedo ver dos formas de extraer los métodos aquí, pero todos ellos parecen mezclar responsabilidades que jgauffin mencionan en his comment: gestión de conexión de base de operaciones, manejo de bases de datos, lógica de negocio (datos de exportación). Diga el método ChangeStatus - es un cierto nivel de abstracción - cambia el estado de un mensaje y no le interesa cómo sucede esto, cómo persiste el mensaje, etc. Tal vez debería usar un patrón de Data Mapper para separar más responsabilidades , pero en este escenario aún bastante simple pensé que me saldría con Active Record. ¿Tal vez todo el diseño es tan enrevesado en este momento, que no veo dónde hacer los cortes?

public void Export(Database dstDb) 
{ 
    try 
    { 
     using (DbConnection connection = dstDb.CreateConnection()) 
     { 
      connection.Open(); 
      using (DbTransaction transaction = connection.BeginTransaction()) 
      { 
       // Export all data here (insert into dstDb) 
       ChangeStatus(MessageStatus.FINISHED); 
       transaction.Commit(); 
      } 
     } 
    } 
    catch (Exception exportEx) 
    { 
     try 
     { 
      ChangeStatus(MessageStatus.ERROR); 
      AddErrorDescription(exportEx.Message); 
     } 
     catch (Exception statusEx) 
     { 
      throw new MessageException("Couldn't export message and set its status to ERROR: " + 
        exportExt.Message + "; " + statusEx.Message, Type, Id, statusEx); 
     } 
     throw new MessageException("Couldn't export message, exception: " + exportEx.Message, Type, Id, exportEx); 
    } 
} 

private void ChangeStatus(MessageStatus status) 
{ 
    try 
    { 
     Status = status; 
     srcDb.UpdateDataSet(drHeader.Table.DataSet, HEADERS, 
      CreateHeaderInsertCommand(), CreateHeaderUpdateCommand(), null); 
    } 
    catch (Exception statusEx) 
    { 
     throw new MessageException("Failed to change message status to " + status + ":" + statusEx.Message, statusEx); 
    } 
} 
+1

Es el precio que paga por crear una aplicación robusta. – ChrisF

+1

De acuerdo con ChrisF. Mi único comentario es que deberías lanzar una subclase de 'Excepción', que ya sabes. Pero tiene dos beneficios, obtienes una señal más clara cuando la atrapas más tarde y puedes mover la construcción de cuerdas a un lugar en lugar de repetirla muchas veces. – unholysampler

+0

¿No sería bueno si el compilador le dijera lo que necesita para capturar – CurtainDog

Respuesta

14
  1. Los conjuntos de datos son la raíz de todo mal;) Trate de usar un ORM lugar.
  2. Lee sobre single responsibility principle. tu código hace muchas cosas diferentes.
  3. No capte excepciones solo para volver a lanzarlas
  4. Use usando en la transacción y conexión.
  5. No es necesario detectar todas las excepciones diferentes cuando todos los manejadores de excepciones hacen lo mismo. Los detalles de la excepción (tipo de excepción y propiedad del mensaje) proporcionarán información.
+0

@ 2) He leído sobre SRP y SOLID muchas veces, pero como dice la cita famosa, la brecha entre la teoría y la práctica ... Bueno, el código detrás del * operador de rendimiento * realmente es una llamada a una función virtual, donde ocurre el procesamiento real, por lo que la responsabilidad de este método es ejecutar todo en una transacción y registrar el resultado, ¿es eso demasiado? – lukem00

+0

@ 3) Si es un consejo general, entonces no podría estar más de acuerdo. ¿Qué sucede si los capturo para volver a lanzarlos, pero también a a) revertir b) registrar datos específicos para excepciones particulares ('ExceptionHelper.LogException (sqlex)') c) decorar la excepción con más información, posiblemente en un nivel diferente de abstracción que el excepción interna? – lukem00

+0

Tu clase tiene estas responsabilidades: a) Administrar la conexión a la base de datos. b) Manejar las operaciones de la base de datos. c) lógica comercial (datos de exportación). Intente refactorizar en tres métodos diferentes, cada uno cuidando uno de los pasos mencionados. – jgauffin

4

El manejo de excepciones es un tema muy discutido y se implementa de muy diversas formas.Hay algunas reglas que trato de cumplir al manejar excepciones:

  1. Nunca arroje System.Exception, generalmente hay suficientes tipos de excepciones para llenar su requerimiento, si no, especialícese. Ver: http://www.fidelitydesign.net/?p=45
  2. Solo arroje una excepción si el método en sí no puede hacer otra cosa que lanzar una excepción. Si un método puede recuperar/manejar las variaciones esperadas de entrada/comportamiento, entonces no arroje una excepción. Lanzar excepciones es un proceso que consume muchos recursos.
  3. Nunca atrape una excepción solo para volver a lanzarlo. Capture y vuelva a lanzar si necesita realizar algún trabajo adicional, como informar la excepción o envolver la excepción en otra excepción (normalmente hago esto para el trabajo de WCF o trabajo transaccional).

Estas son sólo las cosas que yo personalmente, una gran cantidad de desarrolladores que prefieren hacer formas en las que se sienten cómodos ....

+0

@ 2) Soy consciente de la penalización de rendimiento introducida por el manejo de excepciones y tengo algunas contracciones cada vez que lanzo, atrapo o vuelvo a lanzar una excepción, pero definitivamente he visto más situaciones donde un método solo puede recuperarse parcialmente del excepción y luego tendrá que volver a lanzar para informar alguna capa superior. Tal vez esto es un signo de un mal diseño? Pero parece funcionar de esa manera con retrocesos. – lukem00

+0

Haha, +1 para la tira cómica - sí, tengo el mensaje :) – lukem00

9

Adicionalmente a la great answer of jgauffin.

6. no capte excepciones solo para registrarlas. atraparlos en el nivel más alto y registrar todas las excepciones allí.

Editar:

Dado que el registro excepción en todo el lugar tiene al menos estas desventajas: varias veces

  1. La misma excepción se puede registrar, que llena el registro de forma innecesaria. Es difícil decir cuántos errores realmente ocurrieron.
  2. Si la persona que llama capta y maneja la excepción, el llamador aún tiene mensajes de error en el archivo de registro, lo que al menos es confuso.
+0

Ese es definitivamente el enfoque que me gustaría tomar. Probablemente debería aplicar esto a mi 'rollbackEx' (simplemente volver a lanzar con información adicional y dejar que la capa superior lo registre). Sin embargo, en una rara ocasión (como 'statusEx'), quiero tragar la excepción (¿hacer lo incorrecto?) Ya que no es muy grave y dejar una nota en un registro (probablemente debería usar' WarnException' aquí en lugar de ' ErrorException'). – lukem00

+0

Dependiendo de lo que se haga con los registros de excepciones, podría ser útil tener excepciones registradas en diferentes puntos durante el desenrollado de la pila, ya que (1) el hecho de que una excepción haya sido capturada y supuestamente manejada no significa que no será de interés, y (2) si se produce una excepción al desenrollar la pila de una excepción anterior, la última excepción puede ser de mayor interés para el código de llamada, pero quien examina los registros puede estar muy interesado en la excepción anterior también (especialmente porque puede proporcionar pistas sobre las circunstancias que conducen a este último). – supercat

+0

Lo que sería ideal sería si el código de desenrollado de pila que arroja la última excepción pudiera incluir dentro de él una referencia al primero, pero desafortunadamente .net no proporciona ninguna buena manera de hacerlo. Como esto generalmente no es práctico, la mejor alternativa puede ser registrar excepciones en múltiples niveles, pero filtrar los registros redundantes (por ejemplo, hacer que los manejadores internos encueren * distintas * excepciones para el registro, y hacer que el controlador externo los escriba en el registro (las huellas de pila pueden haber crecido mientras las excepciones se filtraban)). – supercat

0

Cree una clase de registro que maneje retrocesos para sus propias fallas (es decir, intenta SQL, si eso falla escribe en el registro de eventos, si eso falla, escribe en un archivo de registro local, etc.).

Además, no recomiendo que detecte un error y arroje uno diferente. Cada vez que lo hace, está perdiendo valiosa información de rastreo/depuración sobre la fuente de excepción original.

public void Export(Database dstDb) 
{ 
    try 
    { 
    using (DbConnection connection = dstDb.CreateConnection()) 
    { 
     connection.Open(); 
     using (DbTransaction transaction = connection.BeginTransaction()) 
     { 
      // Export all data here (insert into dstDb) 
      ChangeStatus(MessageStatus.FINISHED); 
      transaction.Commit(); 
     } 
    } 
    } 
    catch (Exception exportEx) 
    { 
    LogError(exportEx);// create a log class for cross-cutting concerns 
     ChangeStatus(MessageStatus.ERROR); 
     AddErrorDescription(exportEx.Message); 

    throw; // throw preserves original call stack for debugging/logging 
    } 
} 

private void ChangeStatus(MessageStatus status) 
{ 
    try 
    { 
    Status = status; 
    srcDb.UpdateDataSet(drHeader.Table.DataSet, HEADERS, 
     CreateHeaderInsertCommand(), CreateHeaderUpdateCommand(), null); 
    } 
    catch (Exception statusEx) 
    { 
    Log.Error(statusEx); 
    throw; 
    } 
} 

También para cualquier situación donde se siente que hay bloques try/catch adicionales necesarios, hacer que su propio método, si son demasiado feo. Me gusta la respuesta de Stefan Steinegger, donde la llamada de nivel superior en su aplicación es el mejor lugar para atrapar.

Parte del problema que aquí me imagino es que algo es mutable que hace que intentes establecer el estado después de un error. Si puede factorizar su objeto para que esté en un estado constante independientemente de si su transacción funciona o no, no tiene que preocuparse por esa parte.

+0

Estaba siguiendo un consejo que encontré en Code Complete: lanzar excepciones en el nivel correcto de abstracción. Supongo que puedo extraer el rastro de la pila de la excepción interna, así que no pierdo ninguna información. De hecho, estoy agregando algunos detalles adicionales: tipo de mensaje y id. Lanzar una excepción personalizada parece ser demasiado problemático para un método privado, pero de esa manera puedo dejar que surja. – lukem00

+0

Cada mensaje contiene información sobre si se extrajo o no con éxito (más el motivo de la falla en este último caso). No estoy seguro de cómo hacer que el mensaje sea inmutable ayuda aquí? – lukem00

+0

Ya sabes, he leído esa parte sobre la captura en el nivel correcto de abstracción, y no estoy seguro de cómo me siento al respecto en este contexto. Destruye tu código con try/catch/throw something else vs.atrapa todo cerca de la parte superior que registra la excepción e informa una falla. Captura todo contra vs excepciones controladas vs capas de abstracción ... Esto va subjetivo. No he andado el catch-rethrow porque uso catch all, así que no puedo hablar de los beneficios que hay allí. – Maslow

Cuestiones relacionadas