Exceptions: Pourquoi lancer tôt? Pourquoi attraper tard?

156

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?

Timidelynx
la source
104
L'idée derrière "attraper tard" est, plutôt que d'attraper aussi tard que possible, d'attraper le plus tôt possible. Si vous avez un analyseur de fichiers, par exemple, il est inutile de manipuler un fichier introuvable. Que voulez-vous faire avec cela, quel est votre chemin de récupération? Il n'y en a pas, alors ne prenez pas. Allez dans votre lexer, que faites-vous là-bas, comment vous en sortez-vous pour que votre programme puisse continuer? Cela ne peut pas, laissez l'exception passer. Comment votre scanner peut-il gérer cela? Cela ne peut pas, laissez-le passer. Comment le code d'appel peut-il gérer cela? Il peut soit essayer un autre chemin de fichier, soit alerter l'utilisateur.
Phoshi
16
Il existe très peu de cas où une exception NullPointerException (je suppose que c'est ce que NPE signifie) devrait être capturée; si possible, cela devrait être évité en premier lieu. Si vous rencontrez NullPointerExceptions, vous avez un code défectueux qui doit être corrigé. C'est probablement une solution facile aussi.
Phil
6
S'il vous plaît, les gars, avant de suggérer de clore cette question en double, vérifiez que la réponse à l'autre question ne répond pas très bien à cette question.
Doc Brown
1
(Citebot) today.java.net/article/2003/11/20/… Si ce n'est pas l'origine de la citation, veuillez fournir une référence à la source qui, selon vous, est la citation la plus probable.
rwong
1
Juste un rappel pour ceux qui atteignent cette question et qui développent Android. Sur Android, les exceptions doivent être interceptées et gérées localement, dans la même fonction que celle où elles ont été interceptées. En effet, les exceptions ne se propagent pas entre les gestionnaires de messages - votre application sera tuée si cela se produit. Donc, vous ne devriez pas citer ce conseil lorsque vous faites du développement Android.
Rwong

Réponses:

118

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.

Michael Shaw
la source
Oui, je sais déjà, et il est hors de question, que je devrais lancer des exceptions au point de départ de l’erreur. Mais pourquoi ne devrais-je pas attraper le NPE et le laisser monter dans le Stacktrace? Je voudrais toujours attraper le NPE et l'envelopper dans une exception significative. Je ne vois pas non plus d'avantage pour lequel je devrais envoyer une exception DAO à la couche service ou ui. J'attrapais toujours cela au niveau de la couche de service et l'enveloppais dans une exception de service avec des informations détaillées supplémentaires, pourquoi l'appel du service avait échoué.
shylynx
8
@shylynx Attraper une exception puis renvoyer une exception plus significative est une bonne chose à faire. Ce que vous ne devriez pas faire, c'est détecter une exception trop tôt et ne pas la renvoyer. L'erreur sur laquelle le dicton met en garde est de détecter l'exception trop tôt, puis de tenter de la gérer au mauvais niveau dans le code.
Simon B
Rendre le contexte évident au moment de recevoir l'exception facilite la vie des développeurs de votre équipe. Un NPE nécessite une enquête plus approfondie pour comprendre le problème
Michael Shaw
4
@shylynx On pourrait poser la question: "Pourquoi avez-vous un point dans votre code qui peut renvoyer un NullPointerException? Pourquoi ne pas vérifier nullet lever une exception (peut-être une IllegalArgumentException) plus tôt pour que l'appelant sache exactement où le mauvais nulls'est passé?" Je crois que ce serait ce que la partie "lancer tôt" de l'adage suggérerait.
Jpmc26
2
@jpmc J'ai pris le NPE uniquement comme exemple pour souligner les préoccupations relatives aux couches et aux exceptions. Je pourrais également le remplacer par une exception IllegalArgumentException.
shylynx
56

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.

Doval
la source
1
+1 pour expliquer clairement pourquoi les différents niveaux sont importants. Excellent exemple sur l'erreur du système de fichiers.
Juan Carlos Coto
24

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:

static int divide(int dividend, int divisor) throws DivideByZeroException {
    if (divisor == 0)
        throw new DivideByZeroException(); // that's a checked exception indeed

    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    try {
        int res = divide(a, b);
        System.out.println(res);
    } catch (DivideByZeroException e) {
        // checked exception... I'm forced to handle it!
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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:

static int divide(int dividend, int divisor) {
    // throws unchecked ArithmeticException for 0 divisor
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt();
    if (b != 0) {
        int res = divide(a, b);
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Alternativement, vous pouvez aller en plein commando sur le style POO comme ceci:

static class Division {
    final int dividend;
    final int divisor;

    private Division(int dividend, int divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public boolean check() {
        return divisor != 0;
    }

    public int eval() {
        return dividend / divisor;
    }

    public static Division with(int dividend, int divisor) {
        return new Division(dividend, divisor);
    }
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Division d = Division.with(a, b);
    if (d.check()) {
        int res = d.eval();
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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 ArithmeticExceptionappel vient de divideou eval, c’est vous qui devez gérer les exceptions et corriger votre code, car vous avez oublié le code check(). Pour les mêmes raisons, attraper un NullPointerExceptionest 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 retourner null pour des cas d'erreur simples comme ceci:

static Integer divide(int dividend, int divisor) {
    if (divisor == 0) return null;
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Integer res = divide(a, b);
    if (res != null) {
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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!

static interface InputResource {
    String fetchData();
}

static interface OutputResource {
    void writeData(String data);
}

static void doMyBusiness(InputResource in, OutputResource out, int times) {
    for (int i = 0; i < times; i++) {
        System.out.println("fetching data");
        String data = in.fetchData();
        System.out.println("outputting data");
        out.writeData(data);
    }
}

En attendant, quelque part ailleurs, les implémentations concrètes ...

static class ConstantInputResource implements InputResource {
    @Override
    public String fetchData() {
        return "Hello World!";
    }
}

static class FailingInputResourceException extends RuntimeException {
    public FailingInputResourceException(String message) {
        super(message);
    }
}

static class FailingInputResource implements InputResource {
    @Override
    public String fetchData() {
        throw new FailingInputResourceException("I am a complete failure!");
    }
}

static class StandardOutputResource implements OutputResource {
    @Override
    public void writeData(String data) {
        System.out.println("DATA: " + data);
    }
}

Et enfin le code de câblage ... Qui gère les exceptions de ressources concrètes? Celui qui sait à leur sujet!

static void start() {
    InputResource in1 = new FailingInputResource();
    InputResource in2 = new ConstantInputResource();
    OutputResource out = new StandardOutputResource();

    try {
        ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
    }
    catch (FailingInputResourceException e)
    {
        System.out.println(e.getMessage());
        System.out.println("retrying...");
        ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
    }
}

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!

Daniel Dinnyes
la source
Vous avez raison de dire que le code fournissant l'interface en saura davantage sur ce qui peut mal tourner que le code utilisant l'interface, mais supposons qu'une méthode utilise deux ressources du même type d'interface et que les défaillances doivent être traitées différemment? Ou si l'une de ces ressources en interne, en tant que détail d'implémentation inconnu de son créateur, utilise d'autres ressources imbriquées du même type? Avoir la couche métier à jeter WrappedFirstResourceExceptionou WrappedSecondResourceExceptionet obliger la couche "câblage" à regarder à l'intérieur de cette exception pour voir la cause première du problème ...
supercat
... peut être dégueulasse, mais il semblerait préférable de supposer que toute FailingInputResourceexception sera le résultat d'une opération avec in1. 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 un catchqui appelle ensuite la handleExceptionmé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.
Supercat
@supercat je comprends ce que vous dites. Je dirais qu'une implémentation de ressources concrètes est responsable de connaître les exceptions qu'elle peut générer. Il n'est pas obligé de tout spécifier (il existe un comportement dit non défini ), mais il doit éviter toute ambiguïté. De plus, les exceptions d'exécution non contrôlées doivent être documentées. Si cela contredit la documentation, c'est un bogue. Si l'on s'attend à ce que le code de l'appelant fasse quoi que ce soit de sensé concernant une exception, il faut au minimum que la ressource l'enveloppe UnrecoverableInternalException, comme un code d'erreur HTTP 500.
Daniel Dinnyes
@supercat À propos de votre suggestion sur les gestionnaires d'erreurs configurables: exactement! Dans mon dernier exemple, la logique de traitement des erreurs est codée en dur et appelle la doMyBusinessméthode statique . C'était par souci de brièveté et il est parfaitement possible de le rendre plus dynamique. Une telle Handlerclasse serait instanciée avec des ressources d’entrée / sortie et aurait une handleméthode qui reçoit une classe implémentant un ReusableBusinessLogicInterface. 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.
Daniel Dinnyes
10

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:

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}

Ce code peut évidemment renvoyer une exception de référence null si PropertyBest null. Il y a deux choses que nous pourrions faire dans ce cas pour "corriger" cette situation. Nous pourrions:

  • Créez automatiquement PropertyB si nous ne l'avons pas; ou
  • Laisser l'exception s'exprimer jusqu'à la méthode d'appel.

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.

Stephen
la source
Je pense que vous utilisez les développeurs en amont dans le mauvais sens. En outre, vous avez dit que cela violait le principe de responsabilité unique, mais en réalité, de nombreuses initialisations à la demande et la mise en cache des valeurs sont mises en œuvre de cette manière (avec des contrôles de concurrence appropriés à la place bien sûr)
Daniel Dinnyes
Dans votre exemple, qu’en est-il de la vérification de la valeur NULL avant l’opération de soustraction, comme ceciif(PropertyB == null) return 0;
user1451111
1
Pouvez-vous également préciser votre dernier paragraphe, en particulier ce que vous entendez par « couche d'abstraction ».
user1451111
Si nous effectuons un travail d'E / S, la couche d'abstraction pour capturer une exception d'E / S serait l'endroit où nous effectuons le travail. À ce stade, nous avons toutes les informations dont nous avons besoin pour décider si nous voulons réessayer ou envoyer une boîte de message à l'utilisateur ou créer un objet à l'aide d'un ensemble de valeurs par défaut.
Stephen
"Dans votre exemple, que diriez-vous de rechercher la valeur null avant l'opération de soustraction, comme dans le cas suivant (PropertyB == null) renvoie 0;" Beurk. Ce serait dire à la méthode d’appel que j’ai des éléments valides à soustraire. Bien sûr, cela est contextuel mais ce serait une mauvaise pratique de faire mon erreur en vérifiant ici dans la plupart des cas.
Stephen
6

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 .

monstre à cliquet
la source
1
Vous avez écrit: Jetez bientôt, ... attrapez bientôt ...! Pourquoi? C'est une approche complètement opposée, contrairement à «lancer tôt, attraper tard».
shylynx
1
@shylynx Je ne sais pas d'où "jette tôt, attrape tard", mais sa valeur est discutable. Que veut dire exactement "tard"? Là où il est logique d'attraper une exception (le cas échéant) dépend du problème. La seule chose qui est claire, c'est que vous voulez détecter les problèmes (et les jeter) le plus tôt possible.
Doval
2
Je suppose que "rattraper tard" est destiné à contraster la pratique de capturer avant que vous puissiez savoir quoi faire pour corriger l'erreur - par exemple, vous voyez parfois des fonctions qui interceptent tout simplement pour qu'elles puissent imprimer un message d'erreur et ensuite renvoyer l'exception.
@Hurkyl: Un problème avec "catch tard" est que si une exception bouillonne à travers des couches qui n'en savent rien, il peut être difficile pour le code qui est en mesure de faire quelque chose dans la situation de savoir que les choses sont réellement comme attendu. Comme exemple simple, supposons que si un analyseur pour un fichier de document utilisateur doit charger un CODEC à partir du disque et qu’une erreur de disque se produise lors de la lecture, le code qui appelle l’analyseur peut agir de manière inappropriée s’il pense qu’une erreur de disque s’affiche lors de la lecture de l’utilisateur. document.
Supercat
4

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.

soru
la source
2

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/ catchcoder 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.).

Aschratt
la source
1

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.

davidk01
la source