¿Son las variables de error un antipatrón o un buen diseño?

43

Para manejar varios posibles errores que no deberían detener la ejecución, tengo una variable error que los clientes pueden verificar y usar para lanzar excepciones. ¿Es este un anti-patrón? ¿Hay una mejor manera de manejar esto? Para ver un ejemplo de esto en acción, puede ver la API mysqli de PHP. Supongamos que los problemas de visibilidad (accesores, ámbito público y privado, ¿es la variable en una clase o global?) Se manejan correctamente.

    
pregunta Trenton Maki 17.06.2014 - 08:22

11 respuestas

61

Si un idioma es inherentemente compatible con las excepciones, entonces se prefiere lanzar excepciones y los clientes pueden detectar la excepción si no desean que se produzca un error. De hecho, los clientes de su código esperan excepciones y se encontrarán con muchos errores porque no verificarán los valores de retorno.

Hay algunas ventajas en el uso de excepciones si tiene una opción.

Messages

Las excepciones contienen mensajes de error legibles por el usuario que los desarrolladores pueden utilizar para la depuración o incluso mostrarlos a los usuarios si así lo desean. Si el código consumidor no puede manejar la excepción, siempre puede iniciar sesión para que los desarrolladores puedan revisar los registros sin tener que detenerse en cada otra traza para averiguar cuál era el valor de retorno y mapearlo en una tabla para averiguar cuál era la excepción real.

Con los valores de retorno, no hay información adicional que pueda proporcionarse fácilmente. Algunos idiomas admitirán realizar llamadas de método para obtener el último mensaje de error, por lo que esta preocupación se alivia un poco, pero eso requiere que la persona que llama realice llamadas adicionales y, a veces, requerirá acceso a un "objeto especial" que transporta esta información.

En el caso de los mensajes de excepción, proporciono la mayor cantidad de contexto posible, como por ejemplo:

  

No se pudo recuperar una política del nombre "foo" para la "barra" del usuario, a la que se hizo referencia en el perfil del usuario.

Compare esto con un código de retorno -85. ¿Cuál preferirías?

Pila de llamadas

Las excepciones generalmente también tienen pilas de llamadas detalladas que ayudan a depurar el código más rápido y más rápido, y también pueden ser registradas por el código de llamada, si así lo desea. Esto permite que los desarrolladores identifiquen el problema generalmente en la línea exacta, y por lo tanto es muy poderoso. Una vez más, compare esto con un archivo de registro con valores de retorno (como un -85, 101, 0, etc.), ¿cuál preferiría?

Falla en el enfoque sesgado rápido

Si se llama a un método en algún lugar que falla, lanzará una excepción. El código de llamada debe suprimir explícitamente la excepción o fallará. Descubrí que esto es realmente sorprendente porque durante el desarrollo y las pruebas (e incluso en la producción) el código falla rápidamente, lo que obliga a los desarrolladores a solucionarlo. En el caso de los valores de retorno, si se omite una verificación de un valor de retorno, el error se ignora silenciosamente y el error aparece en algún lugar inesperado, generalmente con un costo mucho mayor de depuración y corrección.

Empaquetado y desempaquetado de excepciones

Las excepciones se pueden ajustar dentro de otras excepciones y luego se pueden desempaquetar si es necesario. Por ejemplo, su código podría lanzar ArgumentNullException que el código de llamada podría incluir dentro de UnableToRetrievePolicyException porque esa operación falló en el código de llamada. Si bien al usuario se le puede mostrar un mensaje similar al ejemplo que proporcioné anteriormente, algunos códigos de diagnóstico podrían desenvolver la excepción y descubrir que un ArgumentNullException causó el problema, lo que significa que es un error de codificación en el código del consumidor. Esto podría disparar una alerta para que el desarrollador pueda corregir el código. Tales escenarios avanzados no son fáciles de implementar con los valores de retorno.

Simplicidad de código

Este es un poco más difícil de explicar, pero aprendí a través de esta codificación, tanto con valores de retorno como con excepciones. El código que se escribió usando valores de retorno usualmente haría una llamada y luego tendría una serie de verificaciones sobre cuál era el valor de retorno. En algunos casos, llamaría a otro método, y ahora tendrá otra serie de comprobaciones para los valores de retorno de ese método. Con excepciones, el manejo de excepciones es mucho más simple en la mayoría, si no en todos los casos. Tiene un bloque try / catch / finally, con el tiempo de ejecución haciendo todo lo posible para ejecutar el código en los bloques finally para la limpieza. Incluso los bloques de prueba / captura / finalmente anidados son relativamente más fáciles de seguir y mantener que los valores anidados si / else y los valores de retorno asociados de múltiples métodos.

Conclusión

Si la plataforma que está utilizando admite excepciones (especialmente, como Java o .NET), entonces definitivamente no debe asumir que no hay otra forma, excepto para lanzar excepciones porque estas plataformas tienen pautas para lanzar excepciones, y sus clientes lo van a esperar así. Si estuviera usando su biblioteca, no me molestaré en verificar los valores de retorno porque espero que se produzcan excepciones, así es como está el mundo en estas plataformas.

Sin embargo, si fuera C ++, sería un poco más difícil de determinar porque ya existe una base de código grande con códigos de retorno, y una gran cantidad de desarrolladores están sintonizados para devolver valores en lugar de excepciones (por ejemplo, Windows está lleno) con HRESULTs). Además, en muchas aplicaciones, también puede ser un problema de rendimiento (o al menos se percibe que es).

    
respondido por el Omer Iqbal 17.06.2014 - 09:20
20

Las variables de error son una reliquia de lenguajes como C donde las excepciones no estaban disponibles. Hoy en día, debe evitarlos, excepto que está escribiendo una biblioteca que se utiliza potencialmente desde un programa C (o un lenguaje similar sin manejo de excepciones).

Por supuesto, si tiene un tipo de error que podría clasificarse mejor como "advertencia" (= su biblioteca puede proporcionar un resultado válido y la persona que llama puede ignorar la advertencia si piensa que no es importante), entonces un estado El indicador en forma de una variable puede tener sentido incluso en idiomas con excepciones. Pero cuidado, las personas que llaman a la biblioteca tienden a ignorar tales advertencias aunque no deban hacerlo, así que piense dos veces antes de introducir tal construcción en su biblioteca.

    
respondido por el Doc Brown 17.06.2014 - 09:45
16

Hay varias formas de señalar un error:

El problema de la variable de error es que es fácil olvidarse de verificar.

El problema de las excepciones es que crea rutas ocultas de ejecuciones y aunque intentar / capturar es fácil de escribir, garantizar una recuperación adecuada en la cláusula catch es realmente difícil de sacar (no hay soporte de los sistemas / compiladores de tipo).

El problema de los controladores de condición es que no se componen bien: si tiene ejecución de código dinámico (funciones virtuales), es imposible predecir qué condiciones deben manejarse. Además, si se puede plantear la misma condición en varios puntos, no se puede decir que se pueda aplicar una solución uniforme cada vez ... se desordenará rápidamente.

Los retornos polimórficos ( Either a b en Haskell) son mi solución favorita hasta ahora:

  • explícito: no hay ruta oculta de ejecución
  • explícito: totalmente documentado en la firma de tipo de función (sin sorpresa)
  • difícil de ignorar: tienes que hacer un patrón de coincidencia para obtener el resultado deseado y manejar el caso de error

El único problema es que potencialmente pueden llevar a una comprobación excesiva; los idiomas que los usan tienen expresiones idiomáticas para encadenar las llamadas de las funciones que los usan, pero aún puede requerir un poco más de escritura / desorden. En Haskell esto sería mónadas , sin embargo, esto es mucho más aterrador de lo que parece, vea < a href="http://fsharpforfunandprofit.com/posts/recipe-part2/"> Programación orientada a ferrocarriles .

    
respondido por el Matthieu M. 18.06.2014 - 10:32
12

Creo que es horrible. Actualmente estoy refactorizando una aplicación Java que usa valores de retorno en lugar de excepciones. Aunque es posible que no esté trabajando en absoluto con Java, creo que esto se aplica de todas formas.

Terminas con un código como este:

String result = x.doActionA();
if (result != null) {
  throw new Exception(result);
}
result = x.doActionB();
if (result != null) {
  throw new Exception(result);
}

O esto:

if (!x.doActionA()) {
  throw new Exception(x.getError());
}
if (!x.doActionB()) {
  throw new Exception(x.getError());
}

Preferiría que las acciones incluyeran excepciones, por lo que terminas con algo como:

x.doActionA();
x.doActionB();

Puede envolver eso en un try-catch, y obtener el mensaje de la excepción, o puede optar por ignorar la excepción, por ejemplo, cuando está eliminando algo que puede haber desaparecido. También conserva la traza de tu pila, si tienes una. Los métodos en sí mismos también se vuelven más fáciles. En lugar de manejar las excepciones, ellos simplemente lanzan lo que salió mal.

Código actual (horrible):

private String doActionA() {
  try {
    someOperationThatCanGoWrong1();
    someOperationThatCanGoWrong2();
    someOperationThatCanGoWrong3();
    return null;
  } catch(Exception e) {
    return "Something went wrong!";
  }
}

Nuevo y mejorado:

private void doActionA() throws Exception {
  someOperationThatCanGoWrong1();
  someOperationThatCanGoWrong2();
  someOperationThatCanGoWrong3();
}

El seguimiento de Strack se conserva y el mensaje está disponible en la excepción, en lugar del inútil "Algo salió mal".

Por supuesto, puede proporcionar mejores mensajes de error, y debería hacerlo. Pero esta publicación está aquí porque el código actual en el que estoy trabajando es una molestia, y no deberías hacer lo mismo.

    
respondido por el mrjink 17.06.2014 - 08:53
5

"Para manejar varios errores posibles que están ocurriendo, eso no debería detener la ejecución"

Si quiere decir que los errores no deben detener la ejecución de la función actual, sino que deben informarse a la persona que llama de alguna manera, entonces tiene algunas opciones que realmente no se han mencionado. Este caso es realmente más una advertencia que un error. Lanzar / Devolver no es una opción, ya que termina la función actual. Un único parámetro de mensaje de error o retorno solo permite que se produzca como máximo uno de estos errores.

Los dos patrones que he usado son:

  • Una colección de error / advertencia, ya sea pasada o mantenida como una variable miembro. A lo que le agregas cosas y solo sigues procesando. Personalmente, no me gusta este enfoque, ya que siento que desestima a la persona que llama.

  • Pase un objeto de controlador de error / advertencia (o configúrelo como una variable miembro). Y cada error llama a una función miembro del manejador. De esta manera, la persona que llama puede decidir qué hacer con dichos errores que no terminan.

Lo que pase a estas colecciones / manejadores debe contener suficiente contexto para que el error se maneje "correctamente": una cadena suele ser demasiado pequeña, por lo que pasar una instancia de Excepción es a menudo sensata, pero a veces es mal visto (como un abuso de excepciones).

El código typicial que usa un controlador de errores podría tener este aspecto

class MyFunClass {
  public interface ErrorHandler {
     void onError(Exception e);
     void onWarning(Exception e);
  }

  ErrorHandler eh;

  public void canFail(int i) {
     if(i==0) {
        if(eh!=null) eh.onWarning(new Exception("canFail shouldn't be called with i=0"));
     }
     if(i==1) {
        if(eh!=null) eh.onError(new Exception("canFail called with i=1 is fatal");
        throw new RuntimeException("canFail called with i=2");
     }
     if(i==2) {
        if(eh!=null) eh.onError(new Exception("canFail called with i=2 is an error, but not fatal"));
     }
  }
}
    
respondido por el Michael Anderson 18.06.2014 - 03:08
5

A menudo no hay nada de malo en usar este patrón o ese patrón, siempre y cuando uses el patrón que todos los demás usan. En el desarrollo de Objective-C , el patrón más preferido es pasar un puntero donde el método llamado puede depositar un Objeto NSError. Las excepciones se reservan para errores de programación y provocan un bloqueo (a menos que tenga programadores Java o .NET que escriben su primera aplicación para iPhone ). Y esto funciona bastante bien.

    
respondido por el gnasher729 17.06.2014 - 18:10
4

La pregunta ya está respondida, pero no puedo evitarlo.

No puede esperar que Exception proporcione una solución para todos los casos de uso. ¿Hammer a alguien?

Hay casos en que las Excepciones no son el fin de todas y pueden ser todas, por ejemplo, si un método recibe una solicitud y es responsable de validar todos los campos pasados, y no solo el primero, debe pensar que debería hacerlo. será posible indicar la causa del error para más de un campo. También debería ser posible indicar si la naturaleza de la validación impide al usuario ir más lejos o no. Un ejemplo de eso sería una contraseña no segura. Podría mostrar un mensaje al usuario que indique que la contraseña ingresada no es muy segura, pero que es lo suficientemente fuerte.

Podría argumentar que todas estas validaciones podrían lanzarse como una excepción al final del módulo de validación, pero serían códigos de error en cualquier cosa excepto en el nombre.

Entonces, la lección aquí es: las excepciones tienen su lugar, al igual que los códigos de error. Elija sabiamente.

    
respondido por el Alexandre Santos 18.06.2014 - 09:07
4

Hay casos de uso donde los códigos de error son preferibles a las excepciones.

Si su código puede continuar a pesar del error, pero necesita informes, una excepción es una mala elección porque las excepciones terminan el flujo. Por ejemplo, si estás leyendo un archivo de datos y descubres que contiene datos erróneos no terminales, podría ser mejor leer el resto del archivo e informar el error en lugar de fallar.

Las otras respuestas han cubierto por qué las excepciones deberían preferirse a los códigos de error en general.

    
respondido por el Jack Aidley 18.06.2014 - 13:47
2

Definitivamente no hay nada de malo en no usar excepciones cuando las excepciones no son una buena opción.

Cuando no se debe interrumpir la ejecución del código (por ejemplo, actuar sobre una entrada del usuario que puede contener varios errores, como un programa para compilar o un formulario para procesar), encuentro que recopilar errores en variables de error como has_errors y error_messages es de hecho un diseño mucho más elegante que lanzar una excepción en el primer error. Permite encontrar todos los errores en la entrada del usuario sin forzar al usuario a reenviar innecesariamente.

    
respondido por el ikh 07.07.2014 - 20:39
1

En algunos lenguajes de programación dinámicos puede usar tanto valores de error como manejo de excepciones . Esto se hace devolviendo el objeto excepción no sacrificada en lugar del valor de retorno ordinario, que puede verificarse como un valor de error, pero arroja una excepción si no se verifica.

En Perl 6 se realiza a través de fail , que si se encuentra dentro del ámbito no fatal; devuelve una excepción especial no explotada Failure object.

En Perl 5 puedes usar Contextual :: Return puedes hacerlo con return FAIL .

    
respondido por el Jakub Narębski 17.06.2014 - 15:32
-1

A menos que haya algo muy específico, creo que tener una variable de error para validación es una mala idea. El propósito parece ser ahorrar el tiempo dedicado a la validación (solo puede devolver el valor variable)

Pero luego, si cambia algo, tiene que volver a calcular ese valor de todos modos. No puedo decir más sobre detener y lanzar Excepciones.

EDITAR: no me di cuenta de que se trata de un paradigma de software en lugar de un caso específico.

Permítame aclarar aún más mis puntos en un caso particular mío en el que mi respuesta tendría sentido

  1. Tengo colecciones de objetos de entidad
  2. Tengo servicios web de estilo de procedimiento que funcionan con estos objetos de entidad

Hay dos tipos de errores:

  1. Los errores cuando se procesa en la capa de servicio
  2. Los errores porque hay inconsistencias en los objetos de la entidad

En la capa de servicio, no hay otra opción que usar el objeto Resultado como envoltura, que es la equivalencia de la variable de error. Simular una excepción a través de una llamada de servicio en un protocolo como http es posible, pero definitivamente no es algo bueno. No estoy hablando de este tipo de error y no pensé que este es el tipo de error que se pregunta en esta pregunta.

Estaba pensando en el segundo tipo de error. Y mi respuesta es sobre este segundo tipo de errores. En los objetos de entidad, hay opciones para nosotros, algunas de ellas son

  • utilizando una variable de validación
  • lanza una excepción inmediatamente cuando un campo se configura incorrectamente desde el configurador

Usar la variable de validación es lo mismo que tener un solo método de validación para cada objeto de entidad. En particular, el usuario puede establecer el valor de manera que mantenga al configurador como puramente definidor, sin efectos secundarios (esto suele ser una buena práctica) o puede incorporar la validación en cada definidor y luego guardar el resultado en una variable de validación. La ventaja de esto sería ahorrar tiempo, el resultado de la validación se almacena en la variable de validación para que cuando el usuario llame a validation () varias veces, no sea necesario realizar una validación múltiple.

Lo mejor que se puede hacer en este caso es usar un solo método de validación sin usar siquiera una validación para almacenar el error de validación. Esto ayuda a mantener el setter solo como setter.

    
respondido por el InformedA 17.06.2014 - 08:34