Función que devuelve verdadero / falso vs. nulo cuando tiene éxito y genera una excepción cuando falla

22

Estoy creando una API, una función que carga un archivo. Esta función no devolverá nada / nulo si el archivo se cargó correctamente y genera una excepción cuando hay algún problema.

¿Por qué una excepción y no solo falsa? Porque dentro de una excepción puedo especificar la razón del fallo (falta de conexión, nombre de archivo faltante, contraseña incorrecta, descripción de archivo faltante, etc.). Quería crear una excepción personalizada (con algo de enumeración para ayudar al usuario de la API a manejar todos los errores).

¿Es una buena práctica o es mejor devolver un objeto (con un booleano dentro, un mensaje de error opcional y la enumeración para errores)?

Accollativo
fuente
55
¿Posible duplicado del valor mágico
mosquito
59
Verdadero y falso van bien juntos. El vacío y la excepción van bien juntos. La verdad y la excepción van juntas como los gatos y los impuestos. Algún día podría estar leyendo tu código. Por favor no me hagas esto.
candied_orange
44
Me gustan bastante los patrones en los que se devuelve un objeto que encapsula el éxito o el fracaso. Por ejemplo, Rust tiene Result<T, E>dónde Testá el resultado si tiene éxito y Ees el error si no tiene éxito. Lanzar una excepción (puede ser, no tan seguro en Java) puede ser costoso ya que implica desenrollar la pila de llamadas, pero crear un objeto Exception es barato. Obviamente, respete los patrones establecidos del lenguaje que está utilizando, pero no combine booleanos y excepciones como esta.
Dan Pantry
44
Ten cuidado aquí. Puede que te dirijas por una madriguera de conejo. Cuando su programa es capaz de lidiar con un problema, generalmente es más fácil detectarlo y solucionarlo verificando las condiciones previas. Rara vez detectaría una excepción, examinaría sus datos en código e intentaría nuevamente después de solucionar el problema. Como tal, rara vez es valioso tener muchos tipos de excepciones. Si está tratando de registrar problemas, esto tiene mucho más sentido, pero no espere escribir un montón de bloques de captura con cantidades significativas de código en ellos.
jpmc26
44
@DanPantry En Java, la pila de llamadas se verifica al crear la excepción, no al lanzarla. fillInStackTrace();se llama en el súper constructor, en la claseThrowable
Simon Forsberg

Respuestas:

35

Lanzar una excepción es simplemente una forma adicional de hacer que un método devuelva un valor. La persona que llama puede verificar un valor de retorno tan fácilmente como detectar una excepción y verificar eso. Por lo tanto, decidir entre throwyreturn requiere otros criterios.

Lanzar excepciones a menudo debe evitarse si pone en peligro la eficiencia de su programa (construir un objeto de excepción y desenrollar la pila de llamadas es mucho más trabajo para la computadora que simplemente poner un valor en él). Pero si el propósito de su método es cargar un archivo, entonces el cuello de botella siempre será la red y la E / S del sistema de archivos, por lo que no tiene sentido optimizar el método de retorno.

También es una mala idea lanzar excepciones para lo que debería ser un flujo de control simple (por ejemplo, cuando una búsqueda tiene éxito al encontrar su valor), porque eso viola las expectativas de los usuarios de API. Pero un método que no cumple su propósito es un caso excepcional (o al menos debería serlo), así que no veo ninguna razón para no lanzar una excepción. Y si hace eso, es mejor que lo convierta en una excepción personalizada y más informativa (pero es una buena idea convertirlo en una subclase de una excepción estándar, más general, como IOException).

Kilian Foth
fuente
77
Si es un resultado esperado que una solicitud de carga de archivos falle, me sorprendería una excepción (especialmente si hay un valor de retorno en la firma del método).
hoffmale
1
Tenga en cuenta que la razón para hacer que se extienda una excepción estándar es que muchos (probablemente incluso la mayoría) de las personas que llaman no escriben código para intentar solucionar el problema. Como resultado, la mayoría de las personas que llaman querrán un simple "atrapar este gran grupo de excepciones", sin tener que enumerarlas todas explícitamente.
jpmc26
44
@gbjbaanb Es por eso que se deben usar excepciones cuando realmente hay una situación que no se puede arreglar. Cuando intenta abrir un archivo y el archivo no se puede leer, este es un buen caso para una excepción. Cuando verifica la existencia del archivo, debe usarse un booleano porque es de esperar que el archivo no esté allí.
Malcolm
21
Kilian dice, en el lema recursivo "las excepciones son para situaciones excepcionales", situaciones excepcionales son cuando la función no puede cumplir su propósito. Si se supone que la función lee un archivo, y el archivo no se puede leer, eso es excepcional . Si se supone que la función le dice si existe o no un archivo (no importa el posible TOCTOU), entonces el archivo que no existe no es excepcional porque la función aún puede hacer lo que se supone que debe hacer. Si se supone que la función afirma que existe un archivo, entonces no existe es excepcional porque eso es lo que significa "afirmar".
Steve Jessop
10
Tenga en cuenta que la única diferencia entre una función que le indica si existe un archivo y una función que afirma que existe un archivo es si el caso del archivo no existente es excepcional. La gente a veces habla como si fuera una propiedad de la condición de error, ya sea "excepcional" o no. No es una propiedad de la condición de error, es una propiedad combinada de la condición de error y el propósito de la función en la que se produce. De lo contrario, nunca obtendríamos excepciones.
Steve Jessop
31

No hay absolutamente ninguna razón para regresar trueal éxito si no regresa falseal fracaso. ¿Cómo debería ser el código del cliente?

if (result = tryMyAPICall()) {
    // business logic
}
else {
    // this will *never* happen anyways
}

En este caso, la persona que llama necesita un bloque try-catch de todos modos, pero luego puede escribir mejor:

try {
    result = tryMyAPICall();
    // business logic
    // will only reach this line when no exception
    // no reason to use an if-condition
} catch (SomeException se) { }

Por lo tanto, el truevalor de retorno es completamente irrelevante para la persona que llama. Así que solo mantén el método void.


En general, hay tres formas de diseñar modos fallidos.

  1. Devuelve verdadero / falso
  2. Utilizar voidExcepción de , lanzamiento (marcado)
  3. devuelve un objeto de resultado intermedio .

Volver true/false

Esto se usa en algunas API antiguas, en su mayoría de estilo c. Los inconvenientes son obviuos, no tienes idea de lo que salió mal. PHP hace esto con bastante frecuencia, lo que lleva a un código como este:

if (xyz_parse($data) === FALSE)
   $error = xyz_last_error();

En contextos multiproceso, esto es aún peor.

Lanzar excepciones (marcadas)

Esta es una buena manera de hacerlo. En algunos puntos, puede esperar el fracaso. Java hace esto con sockets. La suposición básica es que una llamada debe tener éxito, pero todos saben que ciertas operaciones pueden fallar. Las conexiones de socket están entre ellas. Entonces la persona que llama se ve obligada a manejar la falla. es un diseño agradable, porque se asegura de que la persona que llama realmente maneje la falla y le brinda a la persona que llama una forma elegante de lidiar con la falla.

Objeto de resultado devuelto

Esta es otra buena manera de manejar esto. A menudo se usa para analizar o simplemente cosas que deben validarse.

ValidationResult result = parser.validate(data);
if (result.isValid())
    // business logic
else
    error = result.getvalidationError();

Niza, lógica limpia para la persona que llama también.

Hay un poco de debate sobre cuándo usar el segundo caso y cuándo usar el tercero. Algunas personas creen que las excepciones deben ser excepcionales y que no debe diseñar teniendo en cuenta la posibilidad de excepciones, y casi siempre utilizará las terceras opciones. Así de fino. Pero hemos verificado las excepciones en Java, por lo que no veo ninguna razón para no usarlas. Utilizo las comprobaciones comprobadas cuando la suposición básica es que la llamada debería éxito (como usar un socket), pero es posible que falle, y uso la tercera opción cuando no está claro si la llamada debe tener éxito (como validar los datos). Pero hay diferentes opiniones sobre esto.

En tu caso, iría con void+ Exception. Espera que la carga del archivo tenga éxito, y cuando no lo hace, es excepcional. Pero la persona que llama se ve obligada a manejar ese modo de falla, y puede devolver una excepción que describa correctamente qué tipo de error ocurrió.

Poligoma
fuente
5

Esto realmente se reduce a si una falla es excepcional o esperada .

Si un error no es algo que generalmente espera, entonces es razonablemente probable que el usuario de la API simplemente llame a su método sin ningún manejo especial de errores. Lanzar una excepción permite que burbujee la pila a un lugar donde se note.

Si, por otro lado, los errores son comunes, debe optimizar para el desarrollador que los está comprobando, y las try-catchcláusulas son un poco más engorrosas que una if/elifo switchserie.

Diseñe siempre una API en la mentalidad de alguien que la esté utilizando.

Xiong Chiamiov
fuente
1
En mi caso, como alguien señaló, debería ser una falla excepcional que el usuario de la API no pueda cargar el archivo.
Accollativo
4

No devuelva un booleano si nunca tiene la intención de regresar false. Simplemente haga el método voidy documente que arrojará una excepción ( IOExceptiones adecuada) en caso de falla.

La razón de esto es que si devuelve un valor booleano, el usuario de su API puede concluir que él / ella puede hacer esto:

if (fileUpload(file) == false) {
    // Handle failure.
}

Eso no funcionará, por supuesto; es decir, existe una incongruencia entre el contrato de su método y su comportamiento. Si arroja una excepción marcada, el usuario del método debe manejar la falla:

try {
    fileUpload(file);
} catch (IOException e) {
    // Handle failure.
}
JeroenHoek
fuente
3

Si su método tiene un valor de retorno, lanzar una excepción podría sorprender a sus usuarios. Si veo que un método devuelve un valor booleano, estoy bastante convencido de que devolverá verdadero si tiene éxito y falso si no lo hace, y estructuraré mi código usando una cláusula if-else. Si su excepción se basará en una enumeración de todos modos, también puede devolver un valor de enumeración, similar a Windows HResults, y mantener un valor de enumeración para cuando el método tenga éxito.

También es molesto tener una excepción de lanzamiento de método cuando no es el paso final de un procedimiento. Tener que escribir un try-catch y desviar el flujo de control al bloque catch es una buena receta para los espaguetis y debe evitarse.

Si avanza con excepciones, intente devolver void en su lugar, los usuarios descubrirán que tiene éxito si no se lanzan excepciones, y ninguno intentará usar una cláusula if-else para desviar el flujo de control.

ccControl
fuente
1
La enumeración fue solo una idea para ayudar al usuario de la API a manejar diferentes tipos de errores sin analizar el mensaje de error. Entonces, desde su punto de vista, en mi caso es mejor devolver la enumeración y agregar una enumeración para "OK".
Accollativo