Cela fait un moment que je réfléchis à ce problème et je me trouve constamment en train de trouver des mises en garde et des contradictions. J'espère donc que quelqu'un pourra produire une conclusion sur ce qui suit:
Privilégier les exceptions aux codes d'erreur
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 en matière de gestion des erreurs consiste à lever des exceptions, plutôt que de renvoyer des codes d'erreur (pas nécessairement un code d'erreur, mais type représentant une erreur).
Mais pour moi cela semble contredire ...
Codage aux interfaces, pas aux implémentations
Nous codons pour des interfaces ou des abstractions afin de réduire le couplage. Nous ne savons pas ou ne voulons pas savoir le type et la mise en œuvre spécifiques d'une interface. Alors, comment pouvons-nous savoir quelles exceptions nous devrions chercher à attraper? L'implémentation peut générer 10 exceptions différentes, ou aucune. Lorsque nous détectons 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 d'indiquer que certaines méthodes génèrent certaines exceptions (Java, par exemple, utilise le throws
mot - clé.) Du point de vue du code appelant, cela semble très bien - nous savons explicitement quelles exceptions nous devrions éventuellement intercepter.
Mais - cela semble suggérer un ...
Abstraction qui fuit
Pourquoi une interface devrait-elle spécifier quelles exceptions peuvent être levées? Que se passe-t-il si l'implémentation n'a pas besoin de lancer une exception ou a besoin de lancer d'autres exceptions? Il n’ya aucun moyen, au niveau de l’interface, de savoir quelles exceptions une implémentation peut vouloir déclencher.
Alors...
De conclure
Pourquoi les exceptions sont-elles préférées quand elles semblent (à mes yeux) contredire les meilleures pratiques en matière de logiciels? Et, si les codes d'erreur sont si mauvais (et que je n'ai pas besoin d'être vendu sur les vices de 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?
Réponses:
Tout d'abord, je ne suis pas d'accord avec cette affirmation:
Ce n'est pas toujours le cas: par exemple, jetez un coup d'œil à Objective-C (avec le framework Foundation). Là, NSError est le moyen privilégié pour gérer les erreurs, malgré l'existence de ce qu'un développeur Java appellerait de véritables exceptions: @try, @catch, @throw, la classe NSException, etc.
Cependant, il est vrai que de nombreuses interfaces perdent leurs abstractions avec les exceptions levées. J’estime que ce n’est pas la faute du style "exception" de propagation / traitement des erreurs. En général, je crois que le meilleur conseil concernant la gestion des erreurs est le suivant:
Traiter l'erreur / l'exception au niveau le plus bas possible, période
Je pense que si on s'en tient à cette règle générale, la quantité de "fuites" d'abstractions peut être très limitée et contenue.
Pour savoir si les exceptions levées par une méthode devraient faire partie de sa déclaration, je pense qu’elles devraient: elles font partie du contrat défini par cette interface: Cette méthode effectue A, ou échoue avec B ou C.
Par exemple, si une classe est un analyseur XML, une partie de sa conception devrait indiquer que le fichier XML fourni est tout simplement faux. En Java, vous le faites normalement en déclarant les exceptions que vous vous attendez à rencontrer et en les ajoutant à la
throws
partie de la déclaration de la méthode. D'autre part, si l'un des algorithmes d'analyse échoue, il n'y a aucune raison de passer cette exception ci-dessus non gérée.Tout se résume à une chose: une bonne conception d'interface. Si vous concevez votre interface suffisamment bien, aucune exception ne devrait vous hanter. Sinon, ce ne sont pas que des 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: certaines langues, par exemple Eiffel, disposent d'autres mécanismes de traitement des erreurs et n'incluent tout simplement pas les capacités de projection. Là, une "exception" de type est automatiquement déclenchée lorsqu'une post-condition pour une routine n'est pas satisfaite.
la source
goto
sont très différentes. Par exemple, les exceptions vont toujours dans le même sens, en aval de la pile d'appels. Deuxièmement, vous protéger des exceptions surprises est exactement la même chose que DRY - par exemple, en C ++, si vous utilisez RAII pour assurer le nettoyage, cela garantit le nettoyage dans tous les cas, pas seulement les exceptions, mais également tous les flux de contrôle normaux. C'est infiniment plus fiable.try/finally
accomplit quelque chose de semblable. Lorsque vous garantissez correctement le nettoyage, vous n'avez pas besoin de considérer les exceptions comme un cas particulier.Je voudrais simplement 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.
Tout en haut de l’esprit, vous pouvez avoir une approche semblable à celle adoptée par Haskell, où les erreurs peuvent être signalées via des types de données abstraits avec plusieurs constructeurs (pensez des discriminations discriminées, ou des pointeurs nuls, mais avec une sécurité typographique et avec la possibilité d’ajouter une syntaxe. fonctions sucre ou aide pour rendre le flux de code bien paraître).
operationThatMightfail est une fonction qui retourne une valeur encapsulée dans un Maybe. Cela fonctionne comme un pointeur nullable, mais la notation do garantit que le tout est évalué à null si l'un de a, b ou c échoue. (et le compilateur vous empêche de créer une exception NullPointerException accidentelle)
Une autre possibilité consiste à transmettre un objet de gestionnaire d'erreur en tant qu'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 le transmettez, et peut être utilisée par cette fonction pour traiter les exceptions où elles se produisent, sans nécessairement devoir rembobiner la pile via des exceptions.
Common LISP fait cela, et le rend possible grâce à un support syntaxique (arguments implicites) et à ce que les fonctions intégrées suivent ce protocole.
la source
Maybe
est.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 à faire en sorte que l'interface spécifie exactement les exceptions pouvant être levées dans quelles circonstances et déclare que les implémentations doivent mapper leur modèle d'exception interne sur cette spécification, en interceptant, en convertissant et en rediffusant les 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 exister d'autres exceptions lorsque des erreurs de bas niveau surviennent ou qu'un manifeste de bogue, et qu'un client ne peut généralement les gérer qu'en affichant un message d'erreur et / ou en arrêtant l'application. Au moins, l'exception peut toujours contenir des informations permettant de diagnostiquer le problème.
En fait, avec les codes d'erreur, pratiquement la même chose finit par se produire, mais de manière plus implicite, et avec beaucoup plus de risques de perte d'informations et d'application dans un état incohérent.
la source
getSQLState
(générique) etgetErrorCode
(spécifique au vendeur). Maintenant, si seulement il avait des sous-classes appropriées ...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 tombent 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 "que quelque chose que je ne peux pas gérer ici s'est produit, il est nécessaire de contacter quelqu'un d'autre pour savoir quoi faire." Un utilisateur qui saisit une entrée non valide n'est pas une exception (qui devrait être traitée localement par l'entrée en demandant à nouveau, etc.).
Un autre cas dégénéré d'utilisation des exceptions que j'ai rencontré concerne des personnes dont la première réponse est "déclenche une exception". Ceci est presque toujours fait sans écrire la capture (règle de base: écrivez la capture en premier, puis la déclaration de projection). Dans les grandes applications, cela devient problématique lorsqu'une exception non capturée bouillonne dans les régions inférieures et fait exploser le programme.
Je ne suis pas anti-exceptions, mais elles semblent être 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'usage auquel ils sont destinés, mais cette affaire n'est pas aussi vaste que certains le pensent.
la source
Nan. Les spécifications d'exception se trouvent 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, c'est bien. Il n'y a rien de fuyant dans la spécification d'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 votre code de traitement des erreurs. Cette répétition est un problème bien plus important que celui rencontré par les exceptions. Vous ne pouvez jamais ignorer une exception en silence, mais les utilisateurs peuvent et le font en silence pour ignorer les codes de retour, ce qui est définitivement une mauvaise chose.
la source
Bien La gestion des exceptions peut avoir sa propre implémentation d'interface. Selon le type de l'exception levée, effectuez les étapes souhaitées.
La solution à votre problème de conception consiste à avoir deux implémentations d'interface / abstraction. Un pour la fonctionnalité et l'autre pour la gestion des exceptions. Et en fonction du type de l'exception interceptée, appelez la classe de type d'exception appropriée.
L'implémentation de codes d'erreur est un moyen orthodoxe de gérer les exceptions. C'est comme l'utilisation de chaîne vs. constructeur de chaînes.
la source
Les exceptions IM-very-HO doivent être évaluées au cas par cas, car en interrompant le flux de contrôle, elles augmenteront inutilement la complexité réelle et perçue de votre code. Mettez de côté la discussion relative au lancement d'exceptions dans vos fonctions - ce qui peut réellement améliorer votre flux de contrôle. Si vous envisagez de lancer des exceptions via des limites d'appels, tenez compte des points suivants:
Permettre à un appelé de rompre votre flux de contrôle peut ne pas fournir d'avantage réel et il peut ne pas y avoir de moyen significatif de traiter l'exception. Par exemple, si l’on applique le modèle Observable (dans un langage tel que C # dans lequel vous avez des événements partout et que
throws
la définition n’est pas explicite ), il n’ya aucune raison réelle de laisser l’observateur interrompre votre flux de contrôle 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 indiqué); Je pense que c'est en fait une norme qu'après avoir remonté 3-6 cadres de pile, une exception non capturée est susceptible de se retrouver dans une section de code qui soit:
Compte tenu de ce qui précède, la décoration des interfaces avec la
throws
sémantique n’est qu’un gain fonctionnel marginal, car de nombreux appelants par le biais de contrats d’interface ne se soucieraient que de votre éventuel échec.Je dirais que cela devient alors une question de goût et de commodité: votre objectif principal est de récupérer votre état à la fois dans l'appelant et l'appelant après une "exception". Par conséquent, si vous avez beaucoup d'expérience dans le déplacement des codes d'erreur (à venir), C, ou si vous travaillez dans un environnement où les exceptions peuvent tourner au diable (C ++), je ne crois pas que le fait de jeter des choses est si important pour une bonne OOP propre que vous ne pouvez pas compter sur votre ancien. motifs si vous êtes mal à l'aise avec elle. Surtout si cela conduit à casser le SoC.
D'un point de vue théorique, je pense qu'une méthode so-kasher de gestion des exceptions peut être dérivée directement du constat que la plupart du temps, l'appelant direct ne se soucie que de ce que vous avez échoué, pas pourquoi. L'appelé jette, une personne très proche au-dessus (2-3 images) intercepte une version superposée, et l'exception réelle est toujours affectée par un gestionnaire d'erreur spécialisé (même s'il ne s'agit que de traçage) - c'est là que l'AOP serait utile, car ces gestionnaires sont susceptibles d'être horizontaux.
la source
Les deux devraient coexister.
Renvoie le code d'erreur lorsque vous anticipez un comportement donné.
Renvoie une exception lorsque vous n'avez pas anticipé certains comportements.
Les codes d'erreur sont normalement associés à un seul message, quand le type d'exception reste, mais un message peut varier
L'exception a une trace de pile, quand le code d'erreur ne le fait pas. Je n'utilise pas de codes d'erreur pour déboguer un système défectueux.
Cela peut être spécifique à JAVA, mais lorsque je déclare mes interfaces, je ne précise pas quelles exceptions peuvent être levées par une implémentation de cette interface, cela n'a aucun sens.
Ceci est entièrement à vous. Vous pouvez essayer d’attraper un type d’exception très spécifique, puis un type plus général
Exception
. Pourquoi ne pas laisser exception propager la pile et la gérer? Sinon, vous pouvez regarder la programmation d'aspects où la gestion des exceptions devient un aspect "enfichable".Je ne comprends pas pourquoi est-ce un problème pour vous? Oui, vous pouvez avoir une implémentation qui n'échoue jamais ou lève des exceptions et vous pouvez avoir une implémentation différente qui échoue et lève constamment des exceptions. 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.
la source
D'après mon expérience, le code qui reçoit l'erreur (que ce soit via une exception, un code d'erreur ou quoi que ce soit d'autre) ne se souciera normalement pas de la cause exacte de l'erreur - il réagirait de la même manière en cas d'échec, sauf en cas de signalement éventuel de l'erreur. error (que ce soit un 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 élément de code sachant comment signaler des erreurs spécifiques (par exemple, formater une chaîne de message), éventuellement en joignant des 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 lancer d'autres exceptions sans signification spécifique.
la source
Je trouve que les exceptions permettent d’écrire un code plus structuré et concis pour signaler et traiter 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'autre part, je conviens que les exceptions révèlent des détails d'implémentation qui doivent être cachés dans le code appelant une interface. Puisqu'il n'est pas possible de savoir a priori quel morceau de code peut lancer quelles exceptions (à moins qu'elles ne soient déclarées dans la signature de la méthode comme en Java), en utilisant des exceptions, nous introduisons des dépendances implicites très complexes entre différentes parties du code, ce qui contre le principe de minimisation des dépendances.
Résumant:
la source
catch Exception
ou mêmeThrowable
, ou équivalent).Soit biaisé à droite.
On ne peut pas l'ignorer, il faut le gérer, c'est complètement transparent. Et si vous utilisez le type d'erreur correct pour gaucher, il transmet les mêmes informations qu'une exception Java.
Inconvénient? Le code avec une gestion correcte des erreurs semble dégoûtant (vrai de tous les mécanismes).
la source