Il existe de nombreuses meilleures pratiques bien connues concernant la gestion des exceptions en isolation. Je connais assez bien les choses à faire et à ne pas faire, mais les choses se compliquent lorsqu'il s'agit de meilleures pratiques ou de modèles dans des environnements plus vastes. "Jette tôt, attrape tard" - j'en ai entendu parler plusieurs fois et cela me déroute toujours.
Pourquoi devrais-je lancer tôt et rattraper tard si, au niveau d'une couche de bas niveau, une exception de pointeur nul est levée? Pourquoi devrais-je l'attraper à une couche supérieure? Il n’est pas logique pour moi d’attraper une exception de bas niveau à un niveau supérieur, tel qu’une couche métier. Cela semble violer les préoccupations de chaque couche.
Imaginez la situation suivante:
J'ai un service qui calcule un chiffre. Pour calculer le chiffre, le service accède à un référentiel pour obtenir des données brutes et à certains autres services pour préparer le calcul. Si quelque chose s'est mal passé au niveau de la couche d'extraction de données, pourquoi devrais-je lancer une exception DataRetrievalException à un niveau supérieur? En revanche, je préférerais que l’exception soit intégrée à une exception significative, par exemple une exception CalculationServiceException.
Pourquoi lancer tôt, pourquoi attraper tard?
la source
Réponses:
D'après mon expérience, il est préférable de lancer des exceptions au point où les erreurs se produisent. Vous faites cela parce que vous en savez le plus sur la raison pour laquelle l'exception a été déclenchée.
Au fur et à mesure que l'exception sauvegarde les couches, la capture et le renvoi sont un bon moyen d'ajouter un contexte supplémentaire à l'exception. Cela peut signifier le lancement d'un type d'exception différent, mais incluez l'exception d'origine lorsque vous effectuez cette opération.
Finalement, l'exception atteindra une couche où vous pourrez prendre des décisions sur le flux de code (par exemple, inviter l'utilisateur à agir). C'est le point où vous devez enfin gérer l'exception et continuer l'exécution normale.
Avec la pratique et l'expérience de votre base de code, il devient assez facile de juger quand ajouter un contexte supplémentaire aux erreurs, et où il est le plus judicieux de gérer, finalement, les erreurs.
Catch → Rethrow
Effectuez cette opération là où vous pouvez utilement ajouter plus d’informations qui éviteraient à un développeur de devoir travailler avec toutes les couches pour comprendre le problème.
Verrou → poignée
Faites ceci où vous pouvez prendre des décisions finales sur ce qui est une exécution appropriée, mais différente, passe par le logiciel.
Capture → Retour d'erreur
Bien que cela soit approprié dans certaines situations, il convient d'envisager le refactoring dans une implémentation Catch → Rethrow, en capturant des exceptions et en renvoyant une valeur d'erreur à l'appelant.
la source
NullPointerException
? Pourquoi ne pas vérifiernull
et lever une exception (peut-être uneIllegalArgumentException
) plus tôt pour que l'appelant sache exactement où le mauvaisnull
s'est passé?" Je crois que ce serait ce que la partie "lancer tôt" de l'adage suggérerait.Vous voulez lancer une exception le plus tôt possible car cela facilite la recherche de la cause. Par exemple, considérons une méthode qui pourrait échouer avec certains arguments. Si vous validez les arguments et échouez au tout début de la méthode, vous savez immédiatement que l'erreur est dans le code appelant. Si vous attendez que les arguments soient nécessaires avant d’échouer, vous devez suivre l’exécution et déterminer si le bogue est dans le code appelant (argument incorrect) ou si la méthode a un bogue. Plus tôt l'exception est levée, plus elle est proche de la cause sous-jacente et plus il est facile de déterminer où les choses se sont mal passées.
La raison pour laquelle les exceptions sont gérées aux niveaux les plus élevés est que les niveaux les plus bas ne savent pas quelle est la marche à suivre appropriée pour gérer l'erreur. En fait, il pourrait y avoir plusieurs manières appropriées de traiter la même erreur en fonction du code appelant. Prenons l'ouverture d'un fichier par exemple. Si vous essayez d'ouvrir un fichier de configuration et qu'il n'y est pas, ignorer l'exception et poursuivre avec la configuration par défaut peut constituer une réponse appropriée. Si vous ouvrez un fichier privé essentiel à l'exécution du programme et qui manque, votre seule option est probablement de fermer le programme.
Envelopper les exceptions dans les bons types est une préoccupation purement orthogonale.
la source
D'autres ont très bien résumé pourquoi lancer tôt . Laissez-moi me concentrer sur le pourquoi attraper la partie tardive , pour laquelle je n'ai pas vu d'explication satisfaisante à mon goût.
SO POURQUOI DES EXCEPTIONS?
Il semble y avoir une grande confusion autour de la raison pour laquelle des exceptions existent. Permettez-moi de partager le grand secret ici: la raison des exceptions et la gestion des exceptions est ... ABSTRACTION .
Avez-vous vu un code comme celui-ci:
Ce n'est pas comment les exceptions devraient être utilisées. Un code comme celui-ci existe dans la vie réelle, mais il s’agit plutôt d’une aberration et constitue vraiment une exception (jeu de mots). La définition de la division par exemple, même en mathématiques pures, est conditionnelle: c'est toujours le "code de l'appelant" qui doit gérer le cas exceptionnel de zéro pour restreindre le domaine d'entrée. C'est moche. C'est toujours pénible pour l'appelant. Néanmoins, dans de telles situations, le modèle check-then-do est la solution naturelle à suivre:
Alternativement, vous pouvez aller en plein commando sur le style POO comme ceci:
Comme vous le voyez, le code de l'appelant a le fardeau de la pré-vérification, mais ne fait aucune gestion des exceptions après. Si un
ArithmeticException
appel vient dedivide
oueval
, c’est vous qui devez gérer les exceptions et corriger votre code, car vous avez oublié le codecheck()
. Pour les mêmes raisons, attraper unNullPointerException
est presque toujours la mauvaise chose à faire.Maintenant, il y a des gens qui disent vouloir voir les cas exceptionnels dans la signature méthode / fonction, c'est-à-dire pour étendre explicitement le domaine de sortie . Ce sont eux qui favorisent les exceptions vérifiées . Bien entendu, la modification du domaine de sortie devrait obliger tout code appelant direct à s'adapter, ce qui serait effectivement réalisé avec des exceptions vérifiées. Mais vous n'avez pas besoin d'exceptions pour ça! C'est pourquoi vous avez des
Nullable<T>
classes génériques , les classes de cas , les types de données algébriques et types d'union . Certaines personnes OO pourraient même préférer retournernull
pour des cas d'erreur simples comme ceci:Techniquement, des exceptions peuvent être utilisées aux fins décrites ci-dessus, mais voici le point: il n'existe aucune exception à une telle utilisation . Les exceptions sont pro abstraction. Les exceptions concernent l'indirection. Les exceptions permettent d'étendre le domaine "résultat" sans rompre les contrats clients directs et sans reporter le traitement des erreurs sur "ailleurs". Si votre code génère des exceptions qui sont gérées par les appelants directs du même code, sans aucune couche d'abstraction entre les deux, alors vous le faites FAUX.
COMMENT ATTRAPER TARD?
Donc nous en sommes là. Je me suis débrouillé pour montrer que l'utilisation d'exceptions dans les scénarios ci-dessus n'était pas la façon dont les exceptions étaient censées être utilisées. Il existe cependant un véritable cas d'utilisation où l'abstraction et l'indirection offertes par la gestion des exceptions sont indispensables. Comprendre un tel usage aidera également à comprendre la recommandation de prise tardive .
Ce cas d'utilisation est: Programmation contre les ressources abstraites ...
Oui, la logique métier doit être programmée contre les abstractions , pas les implémentations concrètes. Le code de "câblage" IOC de niveau supérieur instanciera les implémentations concrètes des ressources abstraites et les transmet à la logique métier. Rien de nouveau ici. Mais les implémentations concrètes de ces ressources abstraites peuvent potentiellement générer leurs propres exceptions spécifiques à l'implémentation , n'est-ce pas?
Alors, qui peut gérer ces exceptions spécifiques à la mise en œuvre? Est-il alors possible de gérer des exceptions spécifiques aux ressources dans la logique métier? Non, ce n'est pas. La logique applicative est programmée contre les abstractions, ce qui exclut la connaissance des détails de ces exceptions spécifiques à l'implémentation.
"Aha!", Vous pourriez dire: "mais c'est pourquoi nous pouvons sous-classer les exceptions et créer des hiérarchies d'exceptions" (consultez M. Spring !). Laissez-moi vous dire que c'est une erreur. Tout d'abord, tout livre raisonnable sur la POO dit que l'héritage concret est mauvais, mais que cette composante essentielle de la machine virtuelle, la gestion des exceptions, est étroitement liée à l'héritage concret. Ironiquement, Joshua Bloch n'aurait pas pu écrire son livre Effective Java avant de pouvoir acquérir l'expérience d'une machine virtuelle Java fonctionnelle, n'est-ce pas ? Il s’agit plus d’un livre de «leçons apprises» pour la prochaine génération. Deuxièmement, et plus important encore, si vous attrapez une exception de haut niveau, comment allez-vous la GÉRER?
PatientNeedsImmediateAttentionException
: devons-nous lui donner une pilule ou lui amputer les jambes!? Que diriez-vous d'une instruction switch sur toutes les sous-classes possibles? Là va ton polymorphisme, là va l'abstraction. Tu as le point.Alors, qui peut gérer les exceptions spécifiques aux ressources? Ce doit être celui qui connaît les concrétions! Celui qui a instancié la ressource! Le code "câblage" bien sûr! Regarde ça:
La logique métier est codée contre les abstractions ... AUCUNE GESTION D'ERREUR DE RESSOURCE CONCRÈTE!
En attendant, quelque part ailleurs, les implémentations concrètes ...
Et enfin le code de câblage ... Qui gère les exceptions de ressources concrètes? Celui qui sait à leur sujet!
Maintenant supporte avec moi. Le code ci-dessus est simpliste. Vous pouvez par exemple dire que vous avez un conteneur d'entreprise / Web d'entreprise avec plusieurs portées de ressources gérées par conteneur IOC, et que vous avez besoin de nouvelles tentatives automatiques et de la réinitialisation des ressources de session ou de portée de demande, etc. La logique de câblage des portées de niveau inférieur peut être configurée en usine créer des ressources, donc ne pas être au courant des implémentations exactes. Seules les portées de niveau supérieur sauraient réellement quelles exceptions ces ressources de niveau inférieur peuvent générer. Maintenant, attends!
Malheureusement, les exceptions n'autorisent l'indirection que sur la pile d'appels, et différentes étendues avec leurs différentes cardinalités s'exécutent généralement sur plusieurs threads différents. Aucun moyen de communiquer à travers cela avec des exceptions. Nous avons besoin de quelque chose de plus puissant ici. Réponse: message async passant . Catch chaque exception à la racine de la portée de niveau inférieur. Ne rien ignorer, ne rien laisser passer. Cela ferme et supprime toutes les ressources créées sur la pile d'appels de l'étendue actuelle. Ensuite, propagez les messages d'erreur aux niveaux supérieurs en utilisant des files de messages / canaux dans la routine de traitement des exceptions, jusqu'à atteindre le niveau où les concrétions sont connues. C'est le gars qui sait comment gérer ça.
SUMMA SUMMARUM
Donc, selon mon interprétation, attraper en retard signifie attraper des exceptions à l'endroit le plus commode O VOUS NE BRISSEZ PAS L'ABSTRACTION . Ne pas attraper trop tôt! Attrapez les exceptions au niveau de la couche où vous créez l'exception concrète en lançant des occurrences des abstractions de ressources, la couche qui connaît les concrétions des abstractions. La couche "câblage".
HTH. Bonne codage!
la source
WrappedFirstResourceException
ouWrappedSecondResourceException
et obliger la couche "câblage" à regarder à l'intérieur de cette exception pour voir la cause première du problème ...FailingInputResource
exception sera le résultat d'une opération avecin1
. En fait, je pense que dans de nombreux cas, la bonne approche serait que la couche de câblage transmette un objet de gestion des exceptions et que la couche de gestion en inclue uncatch
qui appelle ensuite lahandleException
méthode de cet objet . Cette méthode peut rétablir, ou fournir des données par défaut, ou afficher une invite "Abandonner / Réessayer / Échec" et laisser l'opérateur décider quoi faire, etc. en fonction des besoins de l'application.UnrecoverableInternalException
, comme un code d'erreur HTTP 500.doMyBusiness
méthode statique . C'était par souci de brièveté et il est parfaitement possible de le rendre plus dynamique. Une telleHandler
classe serait instanciée avec des ressources d’entrée / sortie et aurait unehandle
méthode qui reçoit une classe implémentant unReusableBusinessLogicInterface
. Vous pouvez ensuite combiner / configurer pour utiliser différentes implémentations de gestionnaire, de ressource et de logique métier dans la couche de câblage quelque part au-dessus d’eux.Pour répondre correctement à cette question, prenons un pas en arrière et posons une question encore plus fondamentale.
Pourquoi avons-nous des exceptions en premier lieu?
Nous lançons des exceptions pour informer l'appelant de notre méthode que nous ne pouvons pas faire ce que l'on nous a demandé de faire. Le type de l'exception explique pourquoi nous ne pouvions pas faire ce que nous voulions faire.
Jetons un coup d'oeil à du code:
Ce code peut évidemment renvoyer une exception de référence null si
PropertyB
est null. Il y a deux choses que nous pourrions faire dans ce cas pour "corriger" cette situation. Nous pourrions:Créer PropertyB ici pourrait être très dangereux. Pour quelle raison cette méthode a-t-elle créé PropertyB? Cela violerait sûrement le principe de responsabilité unique. Selon toute vraisemblance, si PropertyB n’existe pas ici, cela signifie que quelque chose ne va pas. La méthode est appelée sur un objet partiellement construit ou PropertyB a été défini sur null de manière incorrecte. En créant PropertyB ici, nous pourrions cacher un bug beaucoup plus gros qui pourrait nous piquer plus tard, tel qu'un bug causant la corruption des données.
Si, au contraire, nous laissons la référence nulle bouillir, nous avertissons le développeur qui a appelé cette méthode dès que nous le pouvons, que quelque chose s'est mal passé. Une condition préalable essentielle à l'appel de cette méthode a été manquée.
Donc, en fait, nous lançons tôt car cela sépare beaucoup mieux nos préoccupations. Dès qu'une erreur survient, nous en informons les développeurs en amont.
Pourquoi nous "attrapons tard" est une autre histoire. Nous ne voulons pas vraiment attraper tard, nous voulons vraiment attraper dès que nous savons comment gérer le problème correctement. Dans certains cas, ce sera quinze couches d’abstraction plus tard et dans certains cas, ce sera au moment de la création.
Le fait est que nous voulons intercepter l'exception au niveau de la couche d'abstraction qui nous permet de gérer l'exception au point où nous disposons de toutes les informations dont nous avons besoin pour gérer correctement l'exception.
la source
if(PropertyB == null) return 0;
Lancez dès que vous voyez quelque chose d'intéressant pour éviter de mettre des objets dans un état non valide. Ce qui signifie que si un pointeur nul a été passé, vous devez le vérifier rapidement et lancer un NPE avant qu'il ait une chance d'atteindre le niveau bas.
Catch dès que vous savez quoi faire pour corriger l'erreur (ce n'est généralement pas l'endroit où vous lancez sinon vous pouvez simplement utiliser un if-else), si un paramètre non valide a été passé, la couche qui a fourni le paramètre doit en gérer les conséquences .
la source
Une règle de gestion valide est la suivante: "si le logiciel de niveau inférieur ne parvient pas à calculer une valeur, ..."
Cela ne peut être exprimé qu'au niveau supérieur, sinon le logiciel du niveau inférieur essaie de modifier son comportement en fonction de sa propre correction, ce qui ne va aboutir qu'à un nœud.
la source
Tout d’abord, les exceptions concernent des situations exceptionnelles. Dans votre exemple, aucun chiffre ne peut être calculé si les données brutes ne sont pas présentes car elles n'ont pas pu être chargées.
D'après mon expérience, il est judicieux d'abstraire les exceptions en remontant la pile. En règle générale, vous souhaitez effectuer cette opération chaque fois qu'une exception franchit la limite entre deux couches.
En cas d'erreur lors de la collecte de vos données brutes dans la couche de données, générez une exception pour avertir la personne qui a demandé les données. N'essayez pas de contourner ce problème ici. La complexité du code de traitement peut être très élevée. De plus, la couche de données est uniquement responsable de la demande de données, et non de la gestion des erreurs qui surviennent lors de cette opération. C'est ce que l'on entend par "lancer tôt" .
Dans votre exemple, la couche capturante est la couche service. Le service lui-même est une nouvelle couche reposant sur la couche d'accès aux données. Donc, vous voulez attraper l'exception là-bas. Peut-être que votre service dispose d'une infrastructure de basculement et tente de demander les données à un autre référentiel. Si cela échoue également, placez l'exception dans quelque chose que l'appelant du service comprend (s'il s'agit d'un service Web, il peut s'agir d'une erreur SOAP). Définissez l'exception d'origine en tant qu'exception interne pour que les couches ultérieures puissent enregistrer exactement ce qui s'est mal passé.
L'erreur de service peut être interceptée par la couche appelant le service (par exemple, l'interface utilisateur). Et c'est ce que l'on entendait par "attraper tard" . Si vous ne pouvez pas gérer l'exception dans une couche inférieure, relancez-la. Si la couche la plus supérieure ne peut pas gérer l'exception, gérez-la! Cela peut inclure la journalisation ou la présentation.
La raison pour laquelle vous devriez relancer les exceptions (comme décrit ci-dessus en les englobant dans des exeptions plus générales) est que l'utilisateur est très probablement incapable de comprendre qu'il y a une erreur, par exemple un pointeur pointant vers une mémoire non valide. Et il s'en fiche. Son seul souci est que le chiffre ne puisse pas être calculé par le service et ce sont les informations qui doivent lui être affichées.
En allant plus loin, vous pouvez (dans un monde idéal) complètement laisser de côté
try
/catch
coder de l'interface utilisateur. Utilisez plutôt un gestionnaire d’exception global capable de comprendre les exceptions éventuellement émises par les couches inférieures, de les écrire dans un journal et de les encapsuler dans des objets d’erreur contenant des informations significatives (et éventuellement localisées) sur l’erreur. Ces objets peuvent facilement être présentés à l'utilisateur sous la forme de votre choix (boîtes de message, notifications, messages, etc.).la source
Générer des exceptions tôt est généralement une bonne pratique, car vous ne voulez pas que les contrats rompus traversent le code plus loin que nécessaire. Par exemple, si vous vous attendez à ce qu'un paramètre de fonction soit un entier positif, vous devez appliquer cette contrainte au moment de l'appel de la fonction au lieu d'attendre que cette variable soit utilisée ailleurs dans la pile de code.
Je ne peux pas vraiment commenter parce que j'ai mes propres règles et que ça change d'un projet à l'autre. La seule chose que j'essaie de faire est de séparer les exceptions en deux groupes. L'un est réservé à un usage interne et l'autre à un usage externe uniquement. Les exceptions internes sont capturées et gérées par mon propre code et les exceptions externes doivent être gérées par le code qui m'appelle. Il s’agit en gros d’une forme de récupération des choses plus tard, mais pas tout à fait car cela me donne la possibilité de déroger à la règle lorsque cela est nécessaire dans le code interne.
la source