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

44

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

  

Los métodos públicos deben lanzar como máximo una excepción marcada

     

El uso de excepciones comprobadas obliga a los llamadores de métodos a lidiar con los errores, ya sea propagándolos o manejándolos. Esto hace que esas excepciones sean parte de la API del método.

     

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

Otro bit en Sonar tiene esto :

  

Los métodos públicos deben lanzar como máximo una excepción marcada

     

El uso de excepciones comprobadas obliga a los llamadores de métodos a lidiar con los errores, ya sea propagándolos o manejándolos. Esto hace que esas excepciones sean parte de la API del método.

     

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

     

El siguiente código:

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

debe ser refactorizado en:

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

Los métodos de anulación no están controlados por esta regla y pueden lanzar varias excepciones verificadas.

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 debe lanzar un método? más?

¿Es este un estándar bien aceptado?

    
pregunta sdoca 26.11.2014 - 21:56

4 respuestas

31

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 escribes para llamar a delete() se verá así:

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

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

La clave es no escribir el código que hace que quieras escribir un 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:

  

ThrowsCount

     

Restringe las declaraciones de lanzamientos a un recuento específico (1 de forma predeterminada).

     

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

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

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

No hagas que las personas que usan tu clase quieran escribir código incorrecto.

Debo señalar que la importancia de esto se reduce con Java SE 7 y versiones posteriores, ya que una sola instrucción catch puede detectar múltiples excepciones ( Captura de múltiples tipos de excepciones y nuevas excepciones con la verificación de tipos mejorada de Oracle).

Con Java 6 y antes, tendría un código que parecía:

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 que hacen lo mismo, una y otra vez, una vez por cada excepción. ¿Quieres registrar la excepción y volver a lanzarla? De acuerdo. Las mismas líneas de código para cada excepción.

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

Con Java 7, esto no es tan importante porque puedes hacerlo en su lugar:

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

Además, la comprobación de tipo si uno detecta los tipos de excepciones que se lanzan:

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

El verificador de tipos reconocerá que e puede solo ser de los tipos IOException o SQLException . Todavía no estoy demasiado entusiasmado con el uso de este estilo, pero no está causando tan mal código como en Java 6 (donde te obligaría a tener la firma del método como la superclase que se extienden las excepciones). / p>

A pesar de todos estos cambios, muchas herramientas de análisis estático (Sonar, PMD, Checkstyle) siguen aplicando las guías de estilo de Java 6. No es una mala cosa. Tiendo a estar de acuerdo con una advertencia para que still se aplique, pero puede cambiar la prioridad de ellos a mayor o menor según la forma en que su equipo los priorice.

Si se deben marcar o desmarcar las excepciones ... es una cuestión de g r e a t debate que puede encontrar fácilmente Innumerables publicaciones de blog que abordan cada lado del argumento. Sin embargo, si está trabajando con excepciones marcadas, probablemente debería evitar lanzar varios tipos, al menos en Java 6.

    
respondido por el user40980 27.11.2014 - 01:59
21

La razón por la que, idealmente, desearías lanzar solo un tipo de excepción es porque, de lo contrario, violaría la Responsabilidad individual y Principios de inversión de dependencia . Vamos a usar un ejemplo para demostrar.

Digamos que tenemos un método que recupera datos de la persistencia y que la persistencia es un conjunto de archivos. Ya que 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 utiliza 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 de la implementación interna que estamos trabajando para resumir. El código de llamada necesita saber qué tipo de persistencia estamos utilizando, cuando realmente debería preocuparse si se puede recuperar el registro. En su lugar, 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, podemos simplemente envolver esa excepción en un InvalidRecordException y pasarla (o no envolverla, y simplemente lanzar un nuevo InvalidRecordException ). El código externo no sabe o le importa qué tipo de persistencia se está utilizando. Todo está encapsulado.

En lo que respecta a la responsabilidad única, debemos pensar en el código que produce múltiples excepciones no relacionadas. Digamos que tenemos el siguiente método:

public Record parseFile(String filename) throws IOException, ParseException

¿Qué podemos decir acerca de este método? Solo por la firma podemos decir 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 manejar, ya que pueden cambiar si cambia alguna de las responsabilidades. En su lugar, deberíamos dividir los métodos para que tengan una sola 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 dato de String a los datos de análisis desde cualquier fuente: en memoria, archivo, red, etc. También podemos probar parse más fácilmente ahora porque no necesitamos un archivo en el disco para ejecutar pruebas en contra.

A veces realmente hay dos (o más) excepciones que podemos lanzar desde 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.

    
respondido por el cbojar 29.11.2014 - 06:56
3

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

enlace

Dicho esto, uno de los problemas que este tipo mencionaba con las excepciones comprobadas es que (y personalmente me he topado con esto desde el principio con Java) si sigues agregando un montón de excepciones comprobadas a las cláusulas throws en sus declaraciones de métodos, no solo tiene que introducir un código mucho más repetitivo para admitirlo a medida que avanza a métodos de nivel superior, sino que también crea una mayor compatibilidad y rompe la compatibilidad cuando intenta introducir más tipos de excepción para disminuir Métodos de nivel. Si agrega un tipo de excepción marcada a un método de nivel inferior, entonces tiene que volver a ejecutar su código y ajustar otras declaraciones de métodos también.

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 cláusulas throws para usarla solo y luego subir subclases de la misma internamente. De esa manera, puede crear nuevos tipos de excepciones marcadas sin tener que volver a ejecutar todo su código.

Es posible que al autor de este artículo no le haya gustado tanto, pero tiene mucho sentido en mi experiencia personal (sobre todo si puedes consultar cuáles son todas las subclases de todos modos), y apuesto a que por eso los consejos que estás siguiendo dado es para limitar todo a un tipo de excepción marcada cada uno. Lo que es más, es que el consejo que mencionó en realidad permite múltiples tipos de excepciones verificadas en métodos no públicos, lo que tiene mucho sentido si este es su motivo (o incluso de otra manera). Si es solo un método privado o algo similar, no habrás ejecutado la mitad de tu base de código cuando cambies una pequeña cosa.

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

    
respondido por el Panzercrisis 26.11.2014 - 22:40
-1

Lanzar múltiples 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 generalmente sucede 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 nos demos cuenta de que estos métodos PUEDEN y, a veces, saldrán mal, lanzar solo SomethingMightGoWrongException es tan bueno y mejor.

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

Pero si su proyecto utiliza un diseño en el que hay varias excepciones significativas verificadas, 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Á caído, lidiar con eso".

    
respondido por el user470365 29.11.2014 - 15:06

Lea otras preguntas en las etiquetas