web-dev-qa-db-fra.com

Pourquoi une méthode ne devrait-elle pas lancer plusieurs types d'exceptions vérifiées?

Nous utilisons SonarQube pour analyser notre Java et il a cette règle (définie sur critique):

Les méthodes publiques doivent lever au plus une exception vérifiée

L'utilisation d'exceptions vérifiées force les appelants de méthode à traiter les erreurs, soit en les propageant, soit en les traitant. Cela fait que ces exceptions font entièrement partie de l'API de la méthode.

Pour que la complexité des appelants reste raisonnable, les méthodes ne doivent pas lever plus d'un type d'exception vérifiée. "

Un autre bit dans Sonar a this :

Les méthodes publiques doivent lever au plus une exception vérifiée

L'utilisation d'exceptions vérifiées force les appelants de méthode à traiter les erreurs, soit en les propageant, soit en les traitant. Cela fait que ces exceptions font entièrement partie de l'API de la méthode.

Pour que la complexité des appelants reste raisonnable, les méthodes ne doivent pas lever plus d'un type d'exception vérifiée.

Le code suivant:

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

devrait être refactorisé en:

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

Les méthodes de remplacement ne sont pas vérifiées par cette règle et sont autorisées à lever plusieurs exceptions vérifiées.

Je n'ai jamais rencontré cette règle/recommandation dans mes lectures sur la gestion des exceptions et j'ai essayé de trouver des normes, des discussions, etc. sur le sujet. La seule chose que j'ai trouvée est celle de CodeRach: Combien d'exceptions une méthode devrait-elle lever au maximum?

Est-ce une norme bien acceptée?

47
sdoca

Considérons la situation où vous avez le code fourni de:

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

Le danger ici est que le code que vous écrivez pour appeler delete() ressemble à:

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

C'est mauvais aussi. Et il sera intercepté avec une autre règle qui signale la capture de la classe Exception de base.

La clé est de ne pas écrire de code qui vous donne envie d'écrire du mauvais code ailleurs.

La règle que vous rencontrez est assez courante. Checkstyle l'a dans ses règles de conception:

ThrowsCount

Restreint les instructions throws à un nombre spécifié (1 par défaut).

Justification: Les exceptions font partie de l'interface d'une méthode. Déclarer une méthode pour lancer trop d'exceptions enracinées différemment rend la gestion des exceptions onéreuse et conduit à de mauvaises pratiques de programmation telles que l'écriture de code comme catch (Exception ex). Cette vérification oblige les développeurs à placer les exceptions dans une hiérarchie telle que dans le cas le plus simple, un seul type d'exception doit être vérifié par un appelant, mais toutes les sous-classes peuvent être interceptées spécifiquement si nécessaire.

Cela décrit précisément le problème et quel est le problème et pourquoi vous ne devriez pas le faire. Il s'agit d'une norme bien acceptée que de nombreux outils d'analyse statique identifieront et marqueront.

Et tandis que vous mai faites-le selon la conception du langage, et il y a mai fois où c'est la bonne chose à faire, c'est quelque chose que vous devriez voir et allez immédiatement "euh, pourquoi je fais ça?" Il peut être acceptable pour le code interne où tout le monde est suffisamment discipliné pour ne jamais catch (Exception e) {}, mais le plus souvent, j'ai vu des gens faire des économies, en particulier dans des situations internes.

Ne donnez pas envie aux gens qui utilisent votre classe d'écrire du mauvais code.


Je dois souligner que l'importance de ceci est diminuée avec Java SE 7 et versions ultérieures car une seule instruction catch peut intercepter plusieurs exceptions ( Attraper plusieurs types d'exceptions et renvoyer des exceptions avec un type amélioré) Vérification d'Oracle).

Avec Java 6 et avant, vous auriez un code qui ressemblerait à:

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

et

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

ou

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

Aucune de ces options avec Java 6 n'est idéale. La première approche viole SEC . Plusieurs blocs faisant la même chose, encore une fois et encore - une fois pour chaque exception. Vous voulez enregistrer l'exception et la renvoyer? Ok. Les mêmes lignes de code pour chaque exception.

La deuxième option est pire pour plusieurs raisons. Tout d'abord, cela signifie que vous interceptez toutes les exceptions. Un pointeur nul est coincé là-bas (et il ne devrait pas). De plus, vous relancez un Exception ce qui signifie que la signature de la méthode serait deleteSomething() throws Exception ce qui ne fait qu'embrouiller la pile car les personnes utilisant votre code sont maintenant forcées à catch(Exception e).

Avec Java 7, ce n'est pas comme important car vous pouvez faire à la place:

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

De plus, le type vérifiant si on le fait intercepte les types des exceptions levées:

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

Le vérificateur de type reconnaîtra que e peut seulement être de type IOException ou SQLException. Je ne suis toujours pas trop enthousiaste à propos de l'utilisation de ce style, mais il ne cause pas un code aussi mauvais qu'il l'était sous Java 6 (où cela vous obligerait à avoir la signature de la méthode) la superclasse que les exceptions étendent).

Malgré tous ces changements, de nombreux outils d'analyse statique (Sonar, PMD, Checkstyle) appliquent toujours Java 6 guides de style. Ce n'est pas une mauvaise chose. J'ai tendance à être d'accord avec un avertissement à =encore être appliqué, mais vous pouvez changer la priorité sur eux en majeur ou mineur selon la façon dont votre équipe les priorise.

Si les exceptions doivent être vérifiées ou décochées ... c'est une question de greatdébat que l'on peut facilement trouver d'innombrables articles de blog prenant de chaque côté de l'argument. Cependant, si vous travaillez avec des exceptions vérifiées, vous devriez probablement éviter de lancer plusieurs types, au moins sous Java 6.

32
user40980

La raison pour laquelle vous voudriez, idéalement, ne lever qu'un seul type d'exception est que faire autrement enfreint probablement les principes responsabilité unique et inversion de dépendance . Prenons un exemple pour démontrer.

Disons que nous avons une méthode qui récupère les données de la persistance, et que la persistance est un ensemble de fichiers. Puisque nous traitons des fichiers, nous pouvons avoir un FileNotFoundException:

public String getData(int id) throws FileNotFoundException

Maintenant, nous avons un changement dans les exigences, et nos données proviennent d'une base de données. Au lieu d'un FileNotFoundException (puisque nous ne traitons pas de fichiers), nous lançons maintenant un SQLException:

public String getData(int id) throws SQLException

Il nous faudrait maintenant parcourir tout le code qui utilise notre méthode et changer l'exception que nous devons vérifier, sinon le code ne compilera pas. Si notre méthode est appelée loin, cela peut être beaucoup à changer/faire changer les autres. Cela prend beaucoup de temps et les gens ne seront pas contents.

L'inversion de dépendance indique que nous ne devons vraiment pas lever l'une de ces exceptions car elles exposent les détails de l'implémentation interne que nous travaillons à encapsuler. Le code appelant doit savoir quel type de persistance nous utilisons, alors qu'il ne faut vraiment pas se demander si l'enregistrement peut être récupéré. Au lieu de cela, nous devrions lever une exception qui transmet l'erreur au même niveau d'abstraction que nous exposons via notre API:

public String getData(int id) throws InvalidRecordException

Maintenant, si nous modifions l'implémentation interne, nous pouvons simplement encapsuler cette exception dans un InvalidRecordException et la transmettre (ou non l'encapsuler, et simplement lancer un nouveau InvalidRecordException). Le code externe ne sait pas ou ne se soucie pas du type de persistance utilisé. Tout est encapsulé.


En ce qui concerne la responsabilité unique, nous devons penser au code qui lève plusieurs exceptions non liées. Disons que nous avons la méthode suivante:

public Record parseFile(String filename) throws IOException, ParseException

Que dire de cette méthode? Nous pouvons dire juste à partir de la signature qu'il ouvre un fichier et l'analyse. Quand nous voyons une conjonction, comme "et" ou "ou" dans la description d'une méthode, nous savons qu'elle fait plus d'une chose; il a plus d'une responsabilité . Les méthodes avec plus d'une responsabilité sont difficiles à gérer car elles peuvent changer si l'une des responsabilités change. Au lieu de cela, nous devons diviser les méthodes afin qu'elles aient une seule responsabilité:

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

Nous avons retiré la responsabilité de lire le fichier de la responsabilité d'analyser les données. Un effet secondaire de ceci est que nous pouvons maintenant passer n'importe quelles données de chaîne aux données d'analyse de n'importe quelle source: en mémoire, fichier, réseau, etc. Nous pouvons également tester parse plus facilement maintenant parce que nous ne ' t besoin d'un fichier sur le disque pour exécuter des tests.


Parfois, il y a vraiment deux (ou plusieurs) exceptions que nous pouvons lever d'une méthode, mais si nous nous en tenons à SRP et DIP, les moments où nous rencontrons cette situation deviennent plus rares.

22
cbojar

Je me souviens d'avoir joué un peu avec cela lorsque je jouais avec Java il y a quelque temps, mais je n'étais pas vraiment conscient de la distinction entre coché et décoché jusqu'à ce que je lise votre question. J'ai trouvé cet article sur Google assez rapidement, et cela entre dans une partie de la controverse apparente:

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

Cela étant dit, l'un des problèmes que ce type mentionnait avec les exceptions vérifiées est que (et je l'ai personnellement rencontré depuis le début avec Java) si vous continuez à ajouter un tas d'exceptions vérifiées aux clauses throws dans vos déclarations de méthode, non seulement vous devez mettre beaucoup plus de code standard pour le prendre en charge lorsque vous passez à des méthodes de niveau supérieur, mais cela crée également un plus gros gâchis et rompt la compatibilité lorsque vous essayez d'introduire plus de types d'exceptions à réduire -méthodes de niveau. Si vous ajoutez un type d'exception vérifié à une méthode de niveau inférieur, vous devez alors parcourir votre code et ajuster plusieurs autres déclarations de méthode.

Un point d'atténuation mentionné dans l'article - et l'auteur n'a pas aimé cela personnellement - a été de créer une exception de classe de base, de limiter vos clauses throws pour ne l'utiliser que, puis d'en augmenter les sous-classes en interne. De cette façon, vous pouvez créer de nouveaux types d'exceptions vérifiés sans avoir à parcourir tout votre code.

L'auteur de cet article n'a peut-être pas beaucoup aimé, mais cela est parfaitement logique dans mon expérience personnelle (surtout si vous pouvez de toute façon trouver toutes les sous-classes), et je parie que c'est pourquoi le conseil que vous êtes donné est de limiter tout à un type d'exception vérifié chacun. De plus, les conseils que vous avez mentionnés autorisent en fait plusieurs types d'exceptions vérifiées dans les méthodes non publiques, ce qui est parfaitement logique si tel est leur motif (ou même autrement). Si c'est juste une méthode privée ou quelque chose de similaire de toute façon, vous n'allez pas avoir parcouru la moitié de votre base de code lorsque vous changez une petite chose.

Vous avez en grande partie demandé s'il s'agissait d'une norme acceptée, mais entre vos recherches que vous avez mentionnées, cet article raisonnablement réfléchi et le simple fait de parler d'une expérience de programmation personnelle, il ne semble certainement pas se démarquer en aucune façon.

3
Panzercrisis