web-dev-qa-db-fra.com

La manière moderne d'effectuer la gestion des erreurs ...

Je réfléchis à ce problème depuis un certain temps maintenant et je me trouve continuellement à trouver des mises en garde et des contradictions, alors j'espère que quelqu'un pourra tirer une conclusion sur ce qui suit:

Privilégier les exceptions aux codes d'erreur

Pour autant que je sache, après avoir travaillé dans l'industrie pendant quatre ans, lu des livres et des blogs, etc., la meilleure pratique actuelle pour gérer les erreurs est de lever des exceptions, plutôt que de renvoyer des codes d'erreur (pas nécessairement un code d'erreur, mais un type représentant une erreur).

Mais - pour moi, cela semble contredire ...

Codage vers des interfaces, pas des implémentations

Nous codons sur des interfaces ou des abstractions pour réduire le couplage. Nous ne connaissons pas ou ne voulons pas connaître le type et l'implémentation spécifiques d'une interface. Alors, comment pouvons-nous savoir quelles exceptions nous devrions chercher à saisir? L'implémentation peut lever 10 exceptions différentes, ou elle ne peut en déclencher aucune. Lorsque nous interceptons une exception, nous faisons sûrement des hypothèses sur la mise en œuvre?

Sauf si l'interface a ...

Spécifications d'exception

Certains langages permettent aux développeurs de déclarer que certaines méthodes lèvent certaines exceptions (Java par exemple, utilise le mot-clé throws.) Du point de vue du code appelant, cela semble correct - nous savons explicitement quelles exceptions nous pourrions avoir besoin d'attraper.

Mais - cela semble suggérer un ...

Abstraction qui fuit

Pourquoi une interface devrait-elle spécifier quelles exceptions peuvent être levées? Que faire si l'implémentation n'a pas besoin de lever une exception ou doit lever d'autres exceptions? Il n'y a aucun moyen, au niveau de l'interface, de savoir quelles exceptions une implémentation peut vouloir lever.

Donc...

De conclure

Pourquoi les exceptions sont-elles préférées quand elles semblent (à mes yeux) contredire les meilleures pratiques logicielles? Et, si les codes d'erreur sont si mauvais (et je n'ai pas besoin d'être vendu sur les vices des codes d'erreur), existe-t-il une autre alternative? Quel est l'état actuel (ou à venir) de la gestion des erreurs qui répond aux exigences des meilleures pratiques décrites ci-dessus, mais ne repose pas sur un code d'appel vérifiant la valeur de retour des codes d'erreur?

121
RichK

Tout d'abord, je ne suis pas d'accord avec cette affirmation:

Privilégier les exceptions aux codes d'erreur

Ce n'est pas toujours le cas: par exemple, jetez un œil à Objective-C (avec le framework Foundation). Là, NSError est le moyen préféré pour gérer les erreurs, malgré l'existence de ce qu'un développeur Java appellerait de vraies exceptions: @try, @catch, @throw, classe NSException, etc.

Cependant, il est vrai que de nombreuses interfaces fuient leurs abstractions avec les exceptions levées. Je pense que ce n'est pas la faute du style "d'exception" de la propagation/gestion des erreurs. En général, je pense que le meilleur conseil sur la gestion des erreurs est le suivant:

Traitez l'erreur/l'exception au niveau le plus bas possible, période

Je pense que si l'on s'en tient à cette règle de base, la quantité de "fuite" des abstractions peut être très limitée et contenue.

Sur la question de savoir si les exceptions levées par une méthode devraient faire partie de sa déclaration, je crois qu'elles devraient: elles font partie du contrat défini par cette interface: Cette méthode fait A, ou échoue avec B ou C.

Par exemple, si une classe est un analyseur XML, une partie de sa conception doit consister à indiquer que le fichier XML fourni est tout simplement faux. En Java, vous le faites normalement en déclarant les exceptions que vous attendez et en les ajoutant à la partie throws de la déclaration de la méthode. D'un autre côté, si l'un des algorithmes d'analyse échoue, il n'y a aucune raison de passer cette exception ci-dessus sans traitement.

Tout se résume à une seule chose: Bonne conception d'interface. Si vous concevez votre interface assez bien, aucune quantité d'exceptions ne devrait vous hanter. Sinon, ce ne sont pas seulement les exceptions qui vous dérangeraient.

De plus, je pense que les créateurs de Java avaient de très fortes raisons de sécurité pour inclure des exceptions à une déclaration/définition de méthode.

Une dernière chose: certains langages, Eiffel par exemple, ont d'autres mécanismes de gestion des erreurs et n'incluent tout simplement pas les capacités de lancement. Là, une "exception" de type est automatiquement levée lorsqu'une postcondition pour une routine n'est pas satisfaite.

32
K.Steff

Je voudrais juste noter que les exceptions et les codes d'erreur ne sont pas le seul moyen de traiter les erreurs et les chemins de code alternatifs.

Hors de l'esprit, vous pouvez avoir une approche comme celle adoptée par Haskell, où les erreurs peuvent être signalées via des types de données abstraits avec plusieurs constructeurs (pensez à des énumérations discriminées, ou des pointeurs nuls, mais sûrs et avec la possibilité d'ajouter une syntaxe fonctions de sucre ou d'aide pour rendre le flux de code beau).

func x = do
    a <- operationThatMightFail 10
    b <- operationThatMightFail 20
    c <- operationThatMightFail 30
    return (a + b + c)

operationThatMightfail est une fonction qui renvoie une valeur enveloppée dans un Maybe. Il fonctionne comme un pointeur nullable, mais la notation do garantit que le tout est évalué à null si l'un des a, b ou c échoue. (et le compilateur vous protège contre toute erreur NullPointerException accidentelle)

Une autre possibilité consiste à passer un objet gestionnaire d'erreurs comme argument supplémentaire à chaque fonction que vous appelez. Ce gestionnaire d'erreurs a une méthode pour chaque "exception" possible qui peut être signalée par la fonction à laquelle vous la transmettez, et peut être utilisée par cette fonction pour traiter les exceptions là où elles se produisent, sans nécessairement avoir à rembobiner la pile via des exceptions.

LISP commun le fait et le rend possible en ayant un support syntaxique (arguments implicites) et en faisant suivre les fonctions intégrées de ce protocole.

27
hugomg

Oui, les exceptions peuvent provoquer des abstractions qui fuient. Mais les codes d'erreur ne sont-ils pas encore pires à cet égard?

Une façon de résoudre ce problème consiste à demander à l'interface de spécifier exactement quelles exceptions peuvent être levées dans quelles circonstances et de déclarer que les implémentations doivent mapper leur modèle d'exception interne à cette spécification, en interceptant, convertissant et renvoyant des exceptions si nécessaire. Si vous voulez une interface "préfet", c'est la voie à suivre.

En pratique, il suffit généralement de spécifier des exceptions qui font logiquement partie de l'interface et qu'un client peut vouloir intercepter et faire quelque chose. Il est généralement admis qu'il peut y avoir d'autres exceptions lorsque des erreurs de bas niveau se produisent ou qu'un bogue se manifeste, et qu'un client ne peut généralement gérer qu'en affichant un message d'erreur et/ou en fermant l'application. Au moins l'exception peut toujours contenir des informations qui aident à diagnostiquer le problème.

En fait, avec les codes d'erreur, à peu près la même chose finit par se produire, juste de manière plus implicite, et avec beaucoup plus de chances que des informations soient perdues et que l'application se retrouve dans un état incohérent.

8

Beaucoup de bonnes choses ici, je voudrais juste ajouter que nous devrions tous nous méfier du code qui utilise des exceptions dans le cadre du flux de contrôle normal. Parfois, les gens se retrouvent dans ce piège où tout ce qui n'est pas le cas habituel devient une exception. J'ai même vu une exception utilisée comme condition de terminaison de boucle.

Les exceptions signifient "quelque chose que je ne peux pas gérer ici s'est produit, il faut aller voir quelqu'un d'autre pour savoir quoi faire." Un utilisateur tapant une entrée non valide n'est pas une exception (qui doit être géré localement par l'entrée en demandant à nouveau, etc.).

Un autre cas dégénéré d'utilisation d'exception que j'ai vu est celui de personnes dont la première réponse est "lever une exception". Cela se fait presque toujours sans écrire le catch (règle générale: écrivez d'abord le catch, puis l'instruction throw). Dans les grandes applications, cela devient problématique lorsqu'une exception non interceptée émerge des régions du bas et fait exploser le programme.

Je ne suis pas anti-exceptions, mais elles ressemblent à des singletons d'il y a quelques années: utilisées beaucoup trop fréquemment et de manière inappropriée. Ils sont parfaits pour l'utilisation prévue, mais ce cas n'est pas aussi large que certains le pensent.

5
anon

Abstraction qui fuit

Pourquoi une interface devrait-elle spécifier quelles exceptions peuvent être levées? Que faire si l'implémentation n'a pas besoin de lever une exception ou doit lever d'autres exceptions? Il n'y a aucun moyen, au niveau de l'interface, de savoir quelles exceptions une implémentation peut vouloir lever.

Nan. Les spécifications d'exception sont dans le même compartiment que les types de retour et d'argument - elles font partie de l'interface. Si vous ne pouvez pas vous conformer à cette spécification, n'implémentez pas l'interface. Si vous ne lancez jamais, alors ça va. Il n'y a rien de fuyant à spécifier des exceptions dans une interface.

Les codes d'erreur sont plus que mauvais. Ils sont terribles. Vous devez vous rappeler manuellement de les vérifier et de les propager, à chaque fois, pour chaque appel. Cela viole DRY, pour commencer, et fait exploser massivement votre code de gestion des erreurs. Cette répétition est un problème beaucoup plus important que celui auquel sont confrontées les exceptions. Vous ne pouvez jamais ignorer silencieusement une exception, mais les gens peuvent ignorer silencieusement les codes de retour, ce qui est certainement une mauvaise chose.

4
DeadMG

Les exceptions IM-very-HO doivent être jugées au cas par cas, car en cassant le flux de contrôle, elles augmenteront la complexité réelle et perçue de votre code, dans de nombreux cas inutilement. En mettant de côté la discussion relative au lancement d'exceptions à l'intérieur de vos fonctions - ce qui peut en fait améliorer votre flux de contrôle, si l'on veut examiner le lancement d'exceptions à travers les limites des appels, tenez compte des points suivants:

Permettre à un appelé de rompre votre flux de contrôle peut ne fournir aucun avantage réel et il n'existe peut-être aucun moyen significatif de traiter l'exception. Pour un exemple direct, si l'on implémente le modèle Observable (dans un langage tel que C # où vous avez des événements partout et pas de throws explicite dans la définition), il n'y a aucune raison réelle de laisser l'Observer briser votre contrôle couler s'il se bloque, et aucun moyen significatif de gérer leurs affaires (bien sûr, un bon voisin ne devrait pas jeter en observant, mais personne n'est parfait).

L'observation ci-dessus peut être étendue à toute interface faiblement couplée (comme vous l'avez souligné); Je pense que c'est en fait une norme qu'après avoir remonté 3-6 cadres de pile, une exception non interceptée est susceptible de se retrouver dans une section de code qui:

  • est trop abstrait pour traiter l'exception de manière significative, même si l'exception elle-même est transposée;
  • exécute une fonction générique (peu importe pourquoi vous avez échoué, comme une pompe à messages ou l'observable);
  • est spécifique, mais avec une responsabilité différente, et cela ne devrait vraiment pas vous inquiéter;

Compte tenu de ce qui précède, décorer les interfaces avec la sémantique throws n'est qu'un gain fonctionnel marginal, car de nombreux appelants via des contrats d'interface ne s'en soucieraient que si vous échouiez, pas pourquoi.

Je dirais que cela devient alors une question de goût et de commodité: votre objectif principal est de récupérer gracieusement votre état à la fois dans l'appelant et dans l'appelé après une "exception", par conséquent, si vous avez beaucoup d'expérience dans le déplacement de codes d'erreur (à venir à partir d'un arrière-plan C), ou si vous travaillez dans un environnement où les exceptions peuvent devenir mauvaises (C++), je ne pense pas que jeter des trucs soit si important pour Nice, propre OOP que vous ne pouvez pas compter sur vos anciens schémas si vous n'êtes pas à l'aise avec cela. Surtout si cela conduit à briser le SoC.

D'un point de vue théorique, je pense qu'une manière de cacher les SoC peut être dérivée directement de l'observation que la plupart du temps, l'appelant ne se soucie que de votre échec, pas pourquoi. L'appelé lance, quelqu'un très proche au-dessus (2-3 images) attrape une version ascendante, et l'exception réelle est toujours coulée vers un gestionnaire d'erreur spécialisé (même si seul le traçage) - c'est là que l'AOP serait utile, car ces gestionnaires sont susceptibles d'être horizontaux.

2
vski

Eh bien, la gestion des exceptions peut avoir sa propre implémentation d'interface. Selon le type d'exception levée, effectuez les étapes souhaitées.

La solution à votre problème de conception est d'avoir deux implémentations d'interface/abstraction. Un pour la fonctionnalité et l'autre pour la gestion des exceptions. Et selon le type de l'exception interceptée, appelez la classe de type d'exception appropriée.

L'implémentation des codes d'erreur est une manière orthodoxe de gérer les exceptions. C'est comme l'utilisation de string vs string builder.

2
Estefany Velez

Privilégier les exceptions aux codes d'erreur

  • Les deux devraient coexister.

  • Renvoie le code d'erreur lorsque vous anticipez un certain comportement.

  • Renvoie une exception lorsque vous n'avez pas anticipé de comportement.

  • Les codes d'erreur sont normalement associés à un seul message, lorsque le type d'exception reste, mais un message peut varier

  • L'exception a une trace de pile, contrairement au code d'erreur. Je n'utilise pas de codes d'erreur pour déboguer un système défectueux.

Codage vers des interfaces et non des implémentations

Cela peut être spécifique à Java, mais lorsque je déclare mes interfaces, je ne spécifie pas quelles exceptions peuvent être levées par une implémentation de cette interface, cela n'a tout simplement pas de sens.

Lorsque nous interceptons une exception, nous faisons sûrement des hypothèses sur la mise en œuvre?

Cela dépend entièrement de vous. Vous pouvez essayer d'attraper un type d'exception très spécifique, puis attraper un Exception plus général. Pourquoi ne pas laisser l'exception se propager dans la pile, puis la gérer? Alternativement, vous pouvez regarder la programmation des aspects où la gestion des exceptions devient un aspect "enfichable".

Que faire si l'implémentation n'a pas besoin de lever une exception ou doit lever d'autres exceptions?

Je ne comprends pas pourquoi c'est un problème pour toi. Oui, vous pouvez avoir une implémentation qui n'échoue ou ne lève jamais d'exceptions et vous pouvez avoir une implémentation différente qui échoue et lève constamment une exception. Si c'est le cas, ne spécifiez aucune exception sur l'interface et votre problème est résolu.

Cela changerait-il quelque chose si, au lieu d'exception, votre implémentation renvoyait un objet résultat? Cet objet contiendrait le résultat de votre action ainsi que les éventuelles erreurs/échecs. Vous pouvez ensuite interroger cet objet.

2
CodeART

Je trouve que les exceptions permettent d'écrire un code plus structuré et concis pour signaler et gérer les erreurs: l'utilisation de codes d'erreur nécessite de vérifier les valeurs de retour après chaque appel et de décider quoi faire en cas de résultat inattendu.

D'un autre côté, je conviens que les exceptions révèlent des détails d'implémentation qui devraient être cachés au code appelant une interface. Puisqu'il n'est pas possible de savoir a priori quel morceau de code peut lancer quelles exceptions (sauf si elles sont déclarées dans la signature de méthode comme en Java), en utilisant des exceptions, nous introduisons des dépendances implicites très complexes entre les différentes parties du code, ce qui est contre le principe de la minimisation des dépendances.

Résumant:

  • Je pense que les exceptions permettent un code plus propre et une approche plus agressive des tests et du débogage car les exceptions non capturées sont beaucoup plus visibles et difficiles à ignorer que les codes d'erreur (échouent bientôt).
  • En revanche, des bogues d'exception non détectés qui ne sont pas découverts lors des tests peuvent apparaître dans un environnement de production sous la forme d'un plantage. Dans certaines circonstances, ce comportement n'est pas acceptable et dans ce cas, je pense que l'utilisation de codes d'erreur est une approche plus robuste.
1
Giorgio

abstraction qui fuit

Pourquoi une interface devrait-elle spécifier quelles exceptions peuvent être levées? Que faire si l'implémentation n'a pas besoin de lever une exception ou doit lever d'autres exceptions? Il n'y a aucun moyen, au niveau de l'interface, de savoir quelles exceptions une implémentation peut vouloir lever.

D'après mon expérience, le code qui reçoit l'erreur (que ce soit via une exception, un code d'erreur ou toute autre chose) ne se soucierait pas normalement de la cause exacte de l'erreur - il réagirait de la même manière à toute défaillance, sauf pour un éventuel signalement de la erreur (que ce soit une boîte de dialogue d'erreur ou une sorte de journal); et ce rapport serait fait orthogonalement au code qui a appelé la procédure défaillante. Par exemple, ce code pourrait transmettre l'erreur à un autre morceau de code qui sait comment signaler des erreurs spécifiques (par exemple, formater une chaîne de message), en joignant éventuellement certaines informations de contexte.

Bien sûr, dans certains cas, il est nécessaire d'attacher une sémantique spécifique aux erreurs et de réagir différemment en fonction de l'erreur survenue. Ces cas doivent être documentés dans la spécification d'interface. Cependant, l'interface peut toujours se réserver le droit de lever d'autres exceptions sans signification spécifique.

1
Ambroz Bizjak