He estado reflexionando sobre este problema durante un tiempo y me encuentro continuamente encontrando advertencias y contradicciones, así que espero que alguien pueda sacar una conclusión de lo siguiente:
Favorecer excepciones sobre códigos de error
Hasta donde sé, después de trabajar en la industria durante cuatro años, leer libros y blogs, etc., la mejor práctica actual para manejar errores es lanzar excepciones, en lugar de devolver códigos de error (no necesariamente un código de error, sino un tipo que representa un error).
Pero, para mí, esto parece contradecir ...
Codificación para interfaces, no implementaciones
Codificamos interfaces o abstracciones para reducir el acoplamiento. No sabemos, o queremos saber, el tipo específico y la implementación de una interfaz. Entonces, ¿cómo podemos saber qué excepciones deberíamos buscar atrapar? La implementación podría arrojar 10 excepciones diferentes, o podría arrojar ninguna. Cuando detectamos una excepción, ¿estamos haciendo suposiciones sobre la implementación?
A menos que la interfaz tenga ...
Especificaciones de excepción
Algunos lenguajes permiten a los desarrolladores afirmar que ciertos métodos arrojan ciertas excepciones (Java, por ejemplo, usa la throws
palabra clave). Desde el punto de vista del código de llamada, esto parece estar bien: sabemos explícitamente qué excepciones podríamos necesitar detectar.
Pero, esto parece sugerir un ...
Abstracción permeable
¿Por qué una interfaz debe especificar qué excepciones se pueden lanzar? ¿Qué sucede si la implementación no necesita lanzar una excepción, o necesita lanzar otras excepciones? No hay forma, a nivel de interfaz, de saber qué excepciones puede querer lanzar una implementación.
Entonces...
Para concluir
¿Por qué se prefieren las excepciones cuando parecen (en mi opinión) contradecir las mejores prácticas de software? Y, si los códigos de error son tan malos (y no necesito que me vendan los vicios de los códigos de error), ¿hay otra alternativa? ¿Cuál es el estado actual de la técnica (o que pronto será) para el manejo de errores que cumpla con los requisitos de las mejores prácticas descritas anteriormente, pero que no se base en que el código de llamada verifique el valor de retorno de los códigos de error?
Respuestas:
En primer lugar, no estaría de acuerdo con esta afirmación:
Este no es siempre el caso: por ejemplo, eche un vistazo a Objective-C (con el marco de Foundation). Allí, el NSError es la forma preferida de manejar errores, a pesar de la existencia de lo que un desarrollador de Java llamaría excepciones verdaderas: @try, @catch, @throw, clase NSException, etc.
Sin embargo, es cierto que muchas interfaces pierden sus abstracciones con las excepciones lanzadas. Creo que esto no es culpa del estilo de "excepción" de propagación / manejo de errores. En general, creo que el mejor consejo sobre el manejo de errores es este:
Tratar el error / excepción al nivel más bajo posible, período
Creo que si uno se apega a esa regla general, la cantidad de "fuga" de las abstracciones puede ser muy limitada y contenida.
Sobre si las excepciones lanzadas por un método deberían ser parte de su declaración, creo que deberían: son parte del contrato definido por esta interfaz: este método cumple con A o falla con B o C.
Por ejemplo, si una clase es un analizador XML, una parte de su diseño debe ser para indicar que el archivo XML proporcionado es simplemente incorrecto. En Java, normalmente lo hace declarando las excepciones que espera encontrar y agregándolas a la
throws
parte de la declaración del método. Por otro lado, si uno de los algoritmos de análisis falló, no hay razón para pasar esa excepción anterior sin controlar.Todo se reduce a una cosa: buen diseño de interfaz. Si diseñas tu interfaz lo suficientemente bien, ninguna cantidad de excepciones debería atormentarte. De lo contrario, no solo las excepciones te molestarían.
Además, creo que los creadores de Java tenían razones de seguridad muy fuertes para incluir excepciones a una declaración / definición de método.
Una última cosa: algunos idiomas, Eiffel, por ejemplo, tienen otros mecanismos para el manejo de errores y simplemente no incluyen capacidades de lanzamiento. Allí, se genera automáticamente una 'excepción' de clasificación cuando no se cumple una condición posterior para una rutina.
fuente
goto
son muy diferentes. Por ejemplo, las excepciones siempre van en la misma dirección: hacia abajo en la pila de llamadas. Y en segundo lugar, protegerse de las excepciones sorpresa es exactamente la misma práctica que DRY, por ejemplo, en C ++, si usa RAII para garantizar la limpieza, entonces garantiza la limpieza en todos los casos, no solo en la excepción, sino también en todo el flujo de control normal. Esto es infinitamente más confiable.try/finally
logra algo algo similar. Cuando garantiza adecuadamente la limpieza, no necesita considerar las excepciones como un caso especial.Solo me gustaría señalar que las excepciones y los códigos de error no son la única forma de lidiar con los errores y las rutas de código alternativas.
Fuera de la mente, puede tener un enfoque como el adoptado por Haskell, donde los errores se pueden señalar a través de tipos de datos abstractos con múltiples constructores (piense enuminos discriminados o punteros nulos, pero seguros de tipos y con la posibilidad de agregar sintácticos Sugar o funciones auxiliares para que el flujo del código se vea bien).
operationThatMightfail es una función que devuelve un valor envuelto en un Maybe. Funciona como un puntero anulable, pero la notación do garantiza que todo se evalúa como nulo si alguno de a, b o c falla. (y el compilador lo protege de hacer una NullPointerException accidental)
Otra posibilidad es pasar un objeto controlador de errores como argumento adicional a cada función que llame. Este controlador de errores tiene un método para cada posible "excepción" que puede ser señalado por la función a la que se lo pasa, y puede ser utilizado por esa función para tratar las excepciones donde ocurren, sin necesariamente tener que rebobinar la pila a través de excepciones.
Common LISP hace esto y lo hace factible al tener soporte sintáctico (argumentos implícitos) y hacer que las funciones integradas sigan este protocolo.
fuente
Maybe
es.Sí, las excepciones pueden causar abstracciones con fugas. ¿Pero los códigos de error no son aún peores a este respecto?
Una forma de abordar este problema es hacer que la interfaz especifique exactamente qué excepciones se pueden lanzar bajo qué circunstancias y declare que las implementaciones deben asignar su modelo de excepción interno a esta especificación, capturando, convirtiendo y volviendo a lanzar excepciones si es necesario. Si desea una interfaz "perfecta", ese es el camino a seguir.
En la práctica, generalmente es suficiente especificar excepciones que son lógicamente parte de la interfaz y que un cliente puede querer atrapar y hacer algo al respecto. En general, se entiende que puede haber otras excepciones cuando ocurren errores de bajo nivel o se manifiesta un error, y que un cliente solo puede manejar en general mostrando un mensaje de error y / o cerrando la aplicación. Al menos la excepción aún puede contener información que ayude a diagnosticar el problema.
De hecho, con los códigos de error, casi sucede lo mismo, de una manera más implícita y con mucha más probabilidad de que la información se pierda y la aplicación termine en un estado inconsistente.
fuente
getSQLState
(genérico) ygetErrorCode
(específico del proveedor). Ahora, si solo tuviera las subclases adecuadas ...Muchas cosas buenas aquí, solo me gustaría agregar que todos debemos tener cuidado con el código que usa excepciones como parte del flujo de control normal. A veces las personas entran en esa trampa donde cualquier cosa que no sea el caso habitual se convierte en una excepción. Incluso he visto una excepción utilizada como condición de terminación de bucle.
Las excepciones significan "algo que no puedo manejar aquí sucedió, necesito ir a otra persona para averiguar qué hacer". Un usuario que escribe una entrada no válida no es una excepción (la entrada debe manejarlo localmente preguntando de nuevo, etc.).
Otro caso degenerado de uso de excepciones que he visto son las personas cuya primera respuesta es "lanzar una excepción". Esto casi siempre se hace sin escribir el catch (regla general: primero escriba el catch y luego la instrucción throw). En aplicaciones grandes, esto se vuelve problemático cuando una excepción no detectada surge de las regiones inferiores y explota el programa.
No estoy en contra de las excepciones, pero parecen simples de hace unos años: se usan con demasiada frecuencia y de manera inapropiada. Son perfectos para el uso previsto, pero ese caso no es tan amplio como algunos piensan.
fuente
No. Las especificaciones de excepción están en el mismo depósito que los tipos de retorno y argumento: son parte de la interfaz. Si no puede cumplir con esa especificación, no implemente la interfaz. Si nunca lanzas, está bien. No hay nada de fugas al especificar excepciones en una interfaz.
Los códigos de error son más que malos. Son terribles. Debe recordar manualmente comprobarlos y propagarlos, cada vez, para cada llamada. Esto viola DRY, para empezar, y explota masivamente su código de manejo de errores. Esta repetición es un problema mucho mayor que cualquier otra que enfrentan las excepciones. Nunca puede ignorar silenciosamente una excepción, pero la gente puede ignorar silenciosamente los códigos de retorno, definitivamente es algo malo.
fuente
Bueno, el manejo de excepciones puede tener su propia implementación de interfaz. Dependiendo del tipo de excepción lanzada, realice los pasos deseados.
La solución a su problema de diseño es tener dos implementaciones de interfaz / abstracción. Uno para la funcionalidad y el otro para el manejo de excepciones. Y dependiendo del tipo de excepción detectada, llame a la clase de tipo de excepción apropiada.
La implementación de códigos de error es una forma ortodoxa de manejar excepciones. Es como el uso de la cadena frente al generador de cadenas.
fuente
Las excepciones IM-very-HO deben juzgarse caso por caso, porque al romper el flujo de control aumentarán la complejidad real y percibida de su código, en muchos casos innecesariamente. Dejando a un lado la discusión relacionada con el lanzamiento de excepciones dentro de sus funciones, que en realidad puede mejorar su flujo de control, si se trata de lanzar excepciones a través de los límites de llamadas, considere lo siguiente:
Permitir que una persona que llama rompa su flujo de control puede no proporcionar ningún beneficio real, y puede no haber una manera significativa de lidiar con la excepción. Para un ejemplo directo, si uno está implementando el patrón Observable (en un lenguaje como C # donde tiene eventos en todas partes y no hay explícito
throws
en la definición), no hay una razón real para permitir que el Observador rompa su flujo de control si se bloquea, y no hay una manera significativa de lidiar con sus cosas (por supuesto, un buen vecino no debe tirar al observar, pero nadie es perfecto).La observación anterior se puede extender a cualquier interfaz débilmente acoplada (como usted señaló); Creo que en realidad es una norma que después de arrastrar 3-6 cuadros de pila, una excepción no detectada probablemente termine en una sección de código que:
Teniendo en cuenta lo anterior, decorar interfaces con
throws
semántica es solo una ganancia funcional marginal, porque a muchas personas que llaman a través de contratos de interfaz solo les importaría si fallaras, no por qué.Diría que luego se convierte en una cuestión de gusto y conveniencia: su enfoque principal es recuperar con gracia su estado tanto en la persona que llama como en la persona que llama después de una "excepción", por lo tanto, si tiene mucha experiencia en mover códigos de error (próximamente de un fondo C), o si estás trabajando en un entorno donde las excepciones pueden volverse malvadas (C ++), no creo que tirar cosas sea tan importante para una POO agradable y limpia que no puedas confiar en tu viejo patrones si no te sientes cómodo con él. Especialmente si lleva a romper el SoC.
Desde una perspectiva teórica, creo que una forma de manejar las excepciones de SoC-kosher puede derivarse directamente de la observación de que la mayoría de las veces a la persona que llama directamente solo le importa que hayas fallado, no por qué. La persona que llama arroja, alguien muy cerca de arriba (2-3 fotogramas) captura una versión mejorada, y la excepción real siempre se hunde en un controlador de errores especializado (incluso si solo se traza): aquí es donde AOP sería útil, porque estos controladores Es probable que sean horizontales.
fuente
Ambos deberían coexistir.
Devuelve el código de error cuando anticipa cierto comportamiento.
Regrese la excepción cuando no anticipó algún comportamiento.
Los códigos de error normalmente están asociados con un solo mensaje, cuando el tipo de excepción permanece, pero un mensaje puede variar.
La excepción tiene un seguimiento de pila, cuando el código de error no. No uso códigos de error para depurar el sistema roto.
Esto puede ser específico de JAVA, pero cuando declaro mis interfaces no especifico qué excepciones podría generar una implementación de esa interfaz, simplemente no tiene sentido.
Esto es totalmente de usted. Puede intentar capturar un tipo de excepción muy específico y luego capturar un tipo más general
Exception
. ¿Por qué no dejar que la excepción se propague por la pila y luego manejarla? Alternativamente, puede mirar la programación de aspectos donde el manejo de excepciones se convierte en un aspecto "conectable".No entiendo por qué es un problema para ti. Sí, puede tener una implementación que nunca falla o arroja excepciones y puede tener una implementación diferente que falla constantemente y arroja excepciones. Si ese es el caso, no especifique ninguna excepción en la interfaz y su problema se resolverá.
¿Cambiaría algo si en lugar de excepción su implementación devolviera un objeto de resultado? Este objeto contendría el resultado de su acción junto con cualquier error / falla, si corresponde. Luego puedes interrogar a ese objeto.
fuente
En mi experiencia, el código que recibe el error (ya sea a través de una excepción, un código de error o cualquier otra cosa) normalmente no se preocuparía por la causa exacta del error: reaccionaría de la misma manera ante cualquier falla, excepto por un posible informe del error (ya sea un diálogo de error o algún tipo de registro); y este informe se realizaría ortogonalmente al código que llamó el procedimiento de falla. Por ejemplo, este código podría pasar el error a otra pieza de código que sepa cómo informar errores específicos (por ejemplo, formatear una cadena de mensaje), posiblemente adjuntando información de contexto.
Por supuesto, en algunos casos es necesario adjuntar una semántica específica a los errores y reaccionar de manera diferente en función de qué error ocurrió. Tales casos deben documentarse en la especificación de la interfaz. Sin embargo, la interfaz aún puede reservarse el derecho de lanzar otras excepciones sin un significado específico.
fuente
Creo que las excepciones permiten escribir un código más estructurado y conciso para informar y manejar errores: el uso de códigos de error requiere verificar los valores de retorno después de cada llamada y decidir qué hacer en caso de un resultado inesperado.
Por otro lado, estoy de acuerdo en que las excepciones revelan detalles de implementación que deberían ocultarse al código que invoca una interfaz. Dado que no es posible saber a priori qué parte del código puede lanzar qué excepciones (a menos que se declaren en la firma del método como en Java), al utilizar excepciones, estamos introduciendo dependencias implícitas muy complejas entre diferentes partes del código, que es Contra el principio de minimizar las dependencias.
Resumiendo:
fuente
catch Exception
o inclusoThrowable
, o equivalente).Sesgado a la derecha tampoco.
No se puede ignorar, se debe manejar, es completamente transparente. Y SI usa el tipo de error correcto para zurdos, transmite la misma información que una excepción de Java.
¿Abajo? El código con el manejo adecuado de errores se ve desagradable (cierto para todos los mecanismos).
fuente