¿Por qué un método no debería arrojar múltiples tipos de excepciones marcadas?

47

Usamos SonarQube para analizar nuestro código Java y tiene esta regla (establecida en crítica):

Los métodos públicos deberían arrojar como máximo una excepción marcada

El uso de excepciones marcadas obliga a las personas que llaman al método a tratar los errores, ya sea propagándolos o manejándolos. Esto hace que esas excepciones formen parte de la API del método.

Para mantener razonable la complejidad para las personas que llaman, los métodos no deberían arrojar más de un tipo de excepción marcada ".

Otro bit en Sonar tiene esto :

Los métodos públicos deberían arrojar como máximo una excepción marcada

El uso de excepciones marcadas obliga a las personas que llaman al método a tratar los errores, ya sea propagándolos o manejándolos. Esto hace que esas excepciones formen parte de la API del método.

Para mantener razonable la complejidad para las personas que llaman, los métodos no deben arrojar más de un tipo de excepción marcada.

El siguiente código:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

debe refactorizarse en:

public void delete() throws SomeApplicationLevelException {  // Compliant
    /* ... */
}

Los métodos de anulación no están marcados por esta regla y se les permite lanzar varias excepciones marcadas.

Nunca he encontrado esta regla / recomendación en mis lecturas sobre el manejo de excepciones y he tratado de encontrar algunos estándares, discusiones, etc. sobre el tema. Lo único que he encontrado es esto de CodeRach: ¿cuántas excepciones debería arrojar un método como máximo?

¿Es este un estándar bien aceptado?

sdoca
fuente
77
¿Qué le parece? La explicación que citó de SonarQube parece sensata; ¿Tienes alguna razón para dudarlo?
Robert Harvey
3
He escrito mucho código que arroja más de una excepción y he utilizado muchas bibliotecas que también arrojan más de una excepción. Además, en libros / artículos sobre manejo de excepciones, el tema de limitar el número de excepciones generalmente no se menciona. Pero muchos de los ejemplos muestran lanzar / atrapar múltiples dando una aprobación implícita a la práctica. Entonces, encontré la regla sorprendente y quería investigar más sobre las mejores prácticas / filosofía del manejo de excepciones en comparación con los ejemplos básicos de procedimientos.
sdoca

Respuestas:

32

Consideremos la situación en la que tiene el código proporcionado de:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

El peligro aquí es que el código que escriba para llamar delete()se verá así:

try {
  foo.delete()
} catch (Exception e) {
  /* ... */
}

Esto también es malo. Y quedará atrapado con otra regla que marca la captura de la clase de excepción base.

La clave es no escribir código que haga que quieras escribir código incorrecto en otro lugar.

La regla con la que te encuentras es bastante común. Checkstyle lo tiene en sus reglas de diseño:

Lanzamientos

Restringe las declaraciones de lanzamiento a un recuento especificado (1 por defecto).

Justificación: las excepciones forman parte de la interfaz de un método. Declarar un método para generar demasiadas excepciones con raíces diferentes hace que el manejo de excepciones sea oneroso y conduzca a prácticas de programación deficientes, como escribir código como catch (Exception ex). Esta comprobación obliga a los desarrolladores a colocar excepciones en una jerarquía de tal manera que, en el caso más simple, solo una persona que llama necesita verificar un tipo de excepción, pero cualquier subclase puede capturarse específicamente si es necesario.

Esto describe con precisión el problema y cuál es el problema y por qué no debe hacerlo. Es un estándar bien aceptado que muchas herramientas de análisis estático identificarán y marcarán.

Y si bien puede hacerlo de acuerdo con el diseño del lenguaje, y puede haber ocasiones en que sea lo correcto, es algo que debería ver e inmediatamente decir "um, ¿por qué estoy haciendo esto?" Puede ser aceptable para el código interno donde todos son lo suficientemente disciplinados como para nunca catch (Exception e) {}, pero la mayoría de las veces he visto a personas cortar esquinas, especialmente en situaciones internas.

No hagas que las personas que usan tu clase quieran escribir códigos incorrectos.


Debo señalar que la importancia de esto se reduce con Java SE 7 y versiones posteriores porque una sola instrucción catch puede capturar múltiples excepciones ( Capturar múltiples tipos de excepciones y volver a generar excepciones con la Comprobación de tipos mejorada de Oracle).

Con Java 6 y versiones anteriores, tendría un código similar al siguiente:

public void delete() throws IOException, SQLException {
  /* ... */
}

y

try {
  foo.delete()
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

o

try {
    foo.delete()
} catch (Exception ex) {
    logger.log(ex);
    throw ex;
}

Ninguna de estas opciones con Java 6 es ideal. El primer enfoque viola DRY . Múltiples bloques haciendo lo mismo, una y otra vez, una vez para cada excepción. ¿Desea registrar la excepción y volver a lanzarla? Okay. Las mismas líneas de código para cada excepción.

La segunda opción es peor por varias razones. Primero, significa que está capturando todas las excepciones. El puntero nulo queda atrapado allí (y no debería). Además, está volviendo a lanzar una, lo Exceptionque significa que la firma del método sería lo deleteSomething() throws Exceptionque hace un desastre más arriba en la pila, ya que las personas que usan su código ahora están obligadas a hacerlo catch(Exception e).

Con Java 7, esto no es tan importante porque en su lugar puede hacer:

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

Además, el tipo de comprobar si uno qué coger el tipo de las excepciones que es lanzado:

public void rethrowException(String exceptionName)
throws IOException, SQLException {
    try {
        foo.delete();
    } catch (Exception e) {
        throw e;
    }
}

El verificador de tipos reconocerá que soloe puede ser de tipos o . Todavía no estoy demasiado entusiasmado con el uso de este estilo, pero no está causando un código tan malo como lo era en Java 6 (donde te obligaría a que la firma del método sea la superclase que extienden las excepciones).IOExceptionSQLException

A pesar de todos estos cambios, muchas herramientas de análisis estático (Sonar, PMD, Checkstyle) siguen aplicando guías de estilo Java 6. No es algo malo Tiendo a estar de acuerdo con una advertencia de que estos se sigan cumpliendo, pero puede cambiar la prioridad de ellos a mayor o menor según cómo su equipo los priorice.

Si las excepciones deben ser activada o desactivada ... que es una cuestión de g r ae un t debate que uno puede encontrar fácilmente un sinnúmero de publicaciones en el blog que ocupan cada lado del argumento. Sin embargo, si está trabajando con excepciones marcadas, probablemente debería evitar lanzar varios tipos, al menos en Java 6.

otro_paul
fuente
Aceptar esto como la respuesta, ya que responde a mi pregunta sobre si es un estándar bien aceptado. Sin embargo, todavía estoy leyendo las diversas discusiones pro / con a partir del enlace que Panzercrisis proporcionó en su respuesta para determinar cuál será mi estándar.
sdoca
"El verificador de tipos reconocerá que e solo puede ser de los tipos IOException o SQLException": ¿Qué significa esto? ¿Qué sucede cuando se lanza una excepción de otro tipo foo.delete()? ¿Sigue siendo atrapado y arrojado de nuevo?
Giorgio
@Giorgio Será un error de tiempo de compilación si deletearroja una excepción marcada que no sea IOException o SQLException en ese ejemplo. El punto clave que estaba tratando de hacer es que un método que llama a rethrowException seguirá obteniendo el tipo de Exception en Java 7. En Java 6, todo eso se fusiona con el Exceptiontipo común que hace que el análisis estático y otros codificadores sean tristes.
Veo. Me parece un poco complicado. Me resultaría más intuitivo prohibirlo catch (Exception e)y forzarlo a ser uno catch (IOException e)u catch (SQLException e)otro.
Giorgio
@Giorgio Es un paso incremental hacia adelante desde Java 6 para intentar que sea más fácil escribir un buen código. Desafortunadamente, la opción de escribir código incorrecto estará con nosotros durante mucho tiempo. Recuerde que Java 7 puede hacer en su catch(IOException|SQLException ex)lugar. Pero, si solo va a volver a lanzar la excepción, permitir que el verificador de tipo propague el tipo real de la excepción mientras que simplificar el código no es algo malo.
22

La razón por la cual, idealmente, solo querría lanzar un tipo de excepción es porque hacer lo contrario probablemente viola los principios de Inversión de dependencia y responsabilidad única . Usemos un ejemplo para demostrar.

Digamos que tenemos un método que recupera datos de la persistencia, y esa persistencia es un conjunto de archivos. Como estamos tratando con archivos, podemos tener un FileNotFoundException:

public String getData(int id) throws FileNotFoundException

Ahora, tenemos un cambio en los requisitos y nuestros datos provienen de una base de datos. En lugar de un FileNotFoundException(ya que no estamos tratando con archivos), ahora lanzamos un SQLException:

public String getData(int id) throws SQLException

Ahora tendríamos que revisar todo el código que usa nuestro método y cambiar la excepción que tenemos que verificar, de lo contrario el código no se compilará. Si nuestro método se llama a lo largo y ancho, eso puede ser mucho para cambiar / hacer que otros cambien. Lleva mucho tiempo y la gente no va a ser feliz.

La inversión de dependencia dice que realmente no deberíamos lanzar ninguna de estas excepciones porque exponen los detalles internos de implementación que estamos trabajando para encapsular. El código de llamada necesita saber qué tipo de persistencia estamos usando, cuando realmente debería preocuparse si se puede recuperar el registro. En cambio, deberíamos lanzar una excepción que transmita el error al mismo nivel de abstracción que estamos exponiendo a través de nuestra API:

public String getData(int id) throws InvalidRecordException

Ahora, si cambiamos la implementación interna, simplemente podemos envolver esa excepción en una InvalidRecordExceptiony pasarla (o no envolverla, y simplemente lanzar una nueva InvalidRecordException). El código externo no sabe ni le importa qué tipo de persistencia se está utilizando. Todo está encapsulado.


En cuanto a la responsabilidad individual, debemos pensar en el código que arroja múltiples excepciones no relacionadas. Digamos que tenemos el siguiente método:

public Record parseFile(String filename) throws IOException, ParseException

¿Qué podemos decir sobre este método? Podemos decir por la firma que abre un archivo y lo analiza. Cuando vemos una conjunción, como "y" o "o" en la descripción de un método, sabemos que está haciendo más de una cosa; Tiene más de una responsabilidad . Los métodos con más de una responsabilidad son difíciles de administrar, ya que pueden cambiar si alguna de las responsabilidades cambia. En cambio, deberíamos dividir los métodos para que tengan una única responsabilidad:

public String readFile(String filename) throws IOException
public Record parse(String data) throws ParseException

Hemos extraído la responsabilidad de leer el archivo de la responsabilidad de analizar los datos. Un efecto secundario de esto es que ahora podemos pasar cualquier información de cadena a los datos de análisis desde cualquier fuente: en memoria, archivo, red, etc. Ahora también podemos probar parsemás fácilmente porque no necesitamos un archivo en el disco para ejecutar pruebas contra.


A veces hay realmente dos (o más) excepciones que podemos lanzar de un método, pero si nos atenemos a SRP y DIP, las veces que nos encontramos con esta situación se vuelven más raras.

cbojar
fuente
Estoy totalmente de acuerdo con envolver excepciones de nivel inferior según su ejemplo. Hacemos eso regularmente y arrojamos variaciones de MyAppExceptions. Uno de los ejemplos en los que lanzo varias excepciones es cuando intento actualizar un registro en una base de datos. Este método arroja una RecordNotFoundException. Sin embargo, el registro solo se puede actualizar si está en un estado determinado, por lo que el método también arroja una InvalidRecordStateException. Creo que esto es válido y proporciona a la persona que llama información valiosa.
sdoca 01 de
@sdoca Si su updatemétodo es lo más atómico posible, y las excepciones están en el nivel adecuado de abstracción, entonces sí, parece que necesita lanzar dos tipos diferentes de excepciones, ya que hay dos casos excepcionales. Esa debería ser la medida de cuántas excepciones se pueden lanzar, en lugar de estas reglas de linter (a veces arbitrarias).
cbojar
2
Pero si tengo un método que lee datos de una secuencia y los analiza a medida que avanza, no puedo dividir esas dos funciones sin leer la secuencia completa en un búfer, lo que podría ser difícil de manejar. Además, el código que decide cómo manejar adecuadamente las excepciones podría estar separado del código que realiza la lectura y el análisis. Cuando escribo el código de lectura y análisis, no sé cómo el código que llama a mi código podría querer manejar cualquier tipo de Excepción, por lo que necesito dejarlos pasar a ambos.
user3294068
+1: Me gusta mucho esta respuesta, especialmente porque aborda el problema desde un punto de vista de modelado. A menudo no es necesario usar otro idioma (como catch (IOException | SQLException ex)) porque el verdadero problema está en el diseño / modelo del programa.
Giorgio
3

Recuerdo jugar un poco con esto cuando jugué con Java hace un tiempo, pero no era realmente consciente de la distinción entre marcado y no marcado hasta que leí tu pregunta. Encontré este artículo en Google bastante rápido, y entra en algo de la aparente controversia:

http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html

Dicho esto, uno de los problemas que este tipo mencionaba con las excepciones marcadas es que (y personalmente me he encontrado con esto desde el principio con Java) si sigues agregando un montón de excepciones marcadas a las throwscláusulas en tus declaraciones de métodos, no solo ¿tiene que poner mucho más código repetitivo para admitirlo a medida que avanza a métodos de nivel superior, pero también crea un mayor lío y rompe la compatibilidad cuando intenta introducir más tipos de excepción a los métodos de nivel inferior? Si agrega un tipo de excepción marcada a un método de nivel inferior, entonces debe ejecutar nuevamente su código y ajustar también otras declaraciones de métodos.

Un punto de mitigación mencionado en el artículo, y al autor no le gustó esto personalmente, fue crear una excepción de clase base, limitar sus throwscláusulas para usarlo solo y luego aumentar las subclases internamente. De esa manera, puede crear nuevos tipos de excepción marcados sin tener que ejecutar todo su código.

Puede que al autor de este artículo no le haya gustado tanto, pero tiene mucho sentido en mi experiencia personal (especialmente si puedes buscar cuáles son todas las subclases de todos modos), y apuesto a que es por eso que el consejo que te dan es limite todo a un tipo de excepción marcado cada uno. Además, el consejo que mencionó en realidad permite múltiples tipos de excepciones marcadas en métodos no públicos, lo que tiene mucho sentido si este es su motivo (o incluso de otro modo). Si es solo un método privado o algo similar de todos modos, no habrá pasado por la mitad de su base de código cuando cambie una pequeña cosa.

Preguntó en gran medida si este es un estándar aceptado, pero entre su investigación que mencionó, este artículo razonablemente pensado, y hablando solo de la experiencia de programación personal, ciertamente no parece destacarse de ninguna manera.

Panzercrisis
fuente
2
¿Por qué no simplemente declarar tiros Throwabley terminar con ellos, en lugar de inventar toda su propia jerarquía?
Deduplicador
@Dupuplicator Esa es la razón por la que al autor tampoco le gustó la idea; él pensó que también podrías usar todo sin marcar, si vas a hacer eso. Pero si quien usa la API (posiblemente su compañero de trabajo) tiene una lista de todas las excepciones subclasificadas que se derivan de su clase base, puedo ver un poco de beneficio al menos informarles que todas las excepciones esperadas son dentro de un cierto conjunto de subclases; entonces, si sienten que uno de ellos es más "manejable" que los demás, no serían tan propensos a renunciar a un controlador específico para él.
Panzercrisis
La razón por la que las excepciones marcadas en general son mal karma es simple: son virales e infectan a todos los usuarios. Especificar una especificación intencional lo más amplia posible es solo una forma de decir que todas las excepciones están desmarcadas y, por lo tanto, evitar el desorden. Sí, documentar lo que es posible que desee manejar es una buena idea, para la documentación : solo saber qué excepciones pueden provenir de una función tiene un valor estrictamente limitado (aparte de ninguno / tal vez uno, pero Java no lo permite de todos modos) .
Deduplicador
1
@Deduplicator No estoy respaldando la idea, y tampoco necesariamente la favorezco. Solo estoy hablando de la dirección de donde podría haber venido el consejo que dio el OP.
Panzercrisis
1
Gracias por el enlace. Fue un excelente punto de partida para leer sobre este tema.
sdoca
-1

Lanzar varias excepciones marcadas tiene sentido cuando hay varias cosas razonables que hacer.

Por ejemplo, digamos que tienes un método

public void doSomething(Credentials cred, Work work) 
    throws CredentialsRequiredException, TryAgainLaterException{...}

esto viola la regla de excepción de pne, pero tiene sentido.

Desafortunadamente, lo que sucede habitualmente son métodos como

void doSomething() 
    throws IOException, JAXBException,SQLException,MyException {...}

Aquí hay pocas posibilidades de que la persona que llama haga algo específico según el tipo de excepción. Entonces, si queremos empujarlo para que se dé cuenta de que estos métodos PUEDEN y a veces IRÁN mal, arrojar SomethingMightGoWrongException es suficiente y mejor.

Por lo tanto, regla como máximo una excepción marcada.

Pero si su proyecto utiliza un diseño donde hay múltiples excepciones marcadas y significativas, esta regla no debería aplicarse.

Nota al margen: ¿Algo puede salir mal en casi todas partes, por lo que uno puede pensar en usarlo? extiende RuntimeException, pero hay una diferencia entre "todos cometemos errores" y "esto habla del sistema externo y, a veces, estará inactivo, lidia con él".

user470365
fuente
1
"múltiples cosas razonables para hacer" difícilmente cumplen con el srp - este punto fue presentado en una respuesta previa
mosquito
Lo hace. Puede DETECTAR un problema en una función (manejo de un aspecto) y otro problema en otro (también manejo de un aspecto) llamado desde una función que maneja una cosa, es decir, llamar a estas dos funciones. Y la persona que llama puede en una capa de try catch (en una función) MANEJAR un problema y pasar hacia arriba otro y manejar ese solo también.
user470365