web-dev-qa-db-fra.com

On m'a dit que les exceptions ne devraient être utilisées que dans des cas exceptionnels. Comment savoir si mon cas est exceptionnel?

Mon cas spécifique ici est que l'utilisateur peut passer une chaîne dans l'application, celle-ci l'analyse et l'affecte aux objets structurés. Parfois, l'utilisateur peut taper quelque chose de non valide. Par exemple, leur entrée peut décrire une personne mais elle peut dire que son âge est "Apple". Dans ce cas, le comportement correct consiste à annuler la transaction et à signaler à l'utilisateur qu'une erreur s'est produite et qu'il devra réessayer. Il peut être nécessaire de signaler toutes les erreurs que nous pouvons trouver dans l'entrée, pas seulement la première.

Dans ce cas, j'ai soutenu que nous devrions lever une exception. Il n'était pas d'accord, disant: "Les exceptions devraient être exceptionnelles: il est prévu que l'utilisateur puisse entrer des données non valides, donc ce n'est pas un cas exceptionnel" Je ne savais pas vraiment comment argumenter ce point, car par définition du mot semble avoir raison.

Mais, je crois comprendre que c'est pourquoi les exceptions ont été inventées en premier lieu. Auparavant, vous aviez pour inspecter le résultat pour voir si une erreur s'était produite. Si vous ne réussissez pas à vérifier, de mauvaises choses peuvent arriver sans que vous vous en rendiez compte.

Sans exception, chaque niveau de la pile doit vérifier le résultat des méthodes qu'il appelle et si un programmeur oublie de vérifier l'un de ces niveaux, le code pourrait accidentellement continuer et enregistrer des données invalides (par exemple). Semble plus sujette aux erreurs de cette façon.

Quoi qu'il en soit, n'hésitez pas à corriger tout ce que j'ai dit ici. Ma principale question est de savoir si quelqu'un dit que les exceptions devraient être exceptionnelles, comment savoir si mon cas est exceptionnel?

102
Daniel Kaplan

Des exceptions ont été inventées pour faciliter la gestion des erreurs avec moins d'encombrement de code. Vous devez les utiliser dans les cas où ils facilitent la gestion des erreurs avec moins d'encombrement de code. Cette activité "exceptions uniquement pour des circonstances exceptionnelles" découle d'une époque où la gestion des exceptions était considérée comme un impact sur les performances inacceptable. Ce n'est plus le cas dans la grande majorité du code, mais les gens disent toujours la règle sans se rappeler la raison derrière elle.

Surtout en Java, qui est peut-être le langage le plus épris d'exceptions jamais conçu, vous ne devriez pas vous sentir mal à l'idée d'utiliser des exceptions quand cela simplifie votre code. En fait, la propre classe Integer de Java n'a pas de moyen de vérifier si une chaîne est un entier valide sans potentiellement lancer un NumberFormatException.

De plus, bien que vous ne puissiez pas compter uniquement sur la validation de l'interface utilisateur, gardez à l'esprit si votre interface utilisateur est conçue correctement, comme l'utilisation d'un spinner pour saisir des chiffres courts valeurs, alors une valeur non numérique entrant réellement dans le back-end serait une condition exceptionnelle.

88
Karl Bielefeldt

Quand faut-il lever une exception? En ce qui concerne le code, je pense que l'explication suivante est très utile:

ne exception est lorsqu'un membre ne parvient pas à terminer la tâche qu'il est censé effectuer comme indiqué par son nom. (Jeffry Richter, CLR via C #)

Pourquoi est-ce utile? Cela suggère que cela dépend du contexte lorsque quelque chose doit être traité comme une exception ou non. Au niveau des appels de méthode, le contexte est donné par (a) le nom, (b) la signature de la méthode et (b) le code client, qui utilise ou devrait utiliser la méthode.

Pour répondre à votre question, vous devriez jeter un œil au code, où la saisie utilisateur est traitée. Cela pourrait ressembler à ceci:

public void Save(PersonData personData) { … }

Le nom de la méthode suggère-t-il qu'une certaine validation est effectuée? Non. Dans ce cas, une PersonData non valide doit lever une exception.

Supposons que la classe ait une autre méthode qui ressemble à ceci:

public ValidationResult Validate(PersonData personData) { … }

Le nom de la méthode suggère-t-il qu'une certaine validation est effectuée? Oui. Dans ce cas, une PersonData non valide ne doit pas lever d'exception.

Pour mettre les choses ensemble, les deux méthodes suggèrent que le code client devrait ressembler à ceci:

ValidationResult validationResult = personRegister.Validate(personData);
if (validationResult.IsValid())
{
    personRegister.Save(personData)
}
else
{
    // Throw an exception? To answer this look at the context!
    // That is: (a) Method name, (b) signature and
    // (c) where this method is (expected) to be used.
}

Lorsqu'il n'est pas clair si une méthode doit lever une exception, cela peut être dû à un nom ou une signature de méthode mal choisi. Peut-être que la conception de la classe n'est pas claire. Parfois, vous devez modifier la conception du code pour obtenir une réponse claire à la question de savoir si une exception doit être levée ou non.

72
Theo Lenndorff

Les exceptions doivent être exceptionnelles: il est prévu que l'utilisateur puisse entrer des données non valides, ce n'est donc pas un cas exceptionnel

Sur cet argument:

  • On s'attend à ce qu'un fichier n'existe pas, ce n'est donc pas un cas exceptionnel.
  • Il est prévu que la connexion au serveur soit perdue, ce n'est donc pas un cas exceptionnel
  • Il est prévu que le fichier de configuration soit tronqué, ce qui n'est pas un cas exceptionnel
  • Il est prévu que votre demande tombe parfois, ce n'est donc pas un cas exceptionnel

Toute exception que vous attrapez, vous devez vous y attendre car, bien, vous avez décidé de l'attraper. Et donc, selon cette logique, vous ne devez jamais lever d'exceptions que vous prévoyez réellement d'attraper.

Par conséquent, je pense que "les exceptions doivent être exceptionnelles" est une terrible règle d'or.

Ce que vous devez faire dépend de la langue. Différentes langues ont des conventions différentes sur le moment où les exceptions doivent être levées. Python, par exemple, lève des exceptions pour tout et quand en Python, je fais de même. C++, d'autre part, lève relativement peu d'exceptions, et là je fais de même. Vous pouvez traiter C++ ou Java comme Python et lever des exceptions pour tout, mais votre travail ne correspond pas à la façon dont le langage s'attend à être utilisé).

Je préfère l'approche de Python, mais je pense que c'est une mauvaise idée d'y intégrer d'autres langages.

31
Winston Ewert

Je pense toujours à des choses comme accéder au serveur de base de données ou à une API Web lorsque je pense à des exceptions. Vous vous attendez à ce que le serveur/l'API Web fonctionne, mais dans un cas exceptionnel, il se peut que cela ne fonctionne pas (le serveur est en panne). Une demande Web peut généralement être rapide, mais dans des circonstances exceptionnelles (charge élevée), elle peut expirer. C'est quelque chose hors de votre contrôle.

Les données d'entrée de vos utilisateurs sont sous votre contrôle, car vous pouvez vérifier ce qu'ils envoient et en faire ce que vous aimez. Dans votre cas, je validerais l'entrée utilisateur avant même d'essayer de l'enregistrer. Et j'ai tendance à convenir que les utilisateurs fournissant des données non valides doivent être attendus, et votre application doit en tenir compte en validant l'entrée et en fournissant le message d'erreur convivial.

Cela dit, j'utilise des exceptions dans la plupart de mes configurateurs de modèles de domaine, où il ne devrait y avoir aucune chance que des données non valides entrent. Cependant, c'est une dernière ligne de défense, et j'ai tendance à construire mes formulaires d'entrée avec des règles de validation riches , de sorte qu'il n'y a pratiquement aucune chance de déclencher cette exception de modèle de domaine. Ainsi, lorsqu'un passeur attend une chose et en obtient une autre, il s'agit d'une situation exceptionnelle, qui n'aurait pas dû se produire dans des circonstances ordinaires.

EDIT (autre chose à considérer):

Lorsque vous envoyez des données fournies par l'utilisateur à la base de données, vous savez à l'avance ce que vous devez et ne devez pas entrer dans vos tables. Cela signifie que les données peuvent être validées par rapport à un format attendu. C'est quelque chose que vous pouvez contrôler. Ce que vous ne pouvez pas contrôler, c'est que votre serveur tombe en panne au milieu de votre requête. Vous savez donc que la requête est correcte et les données sont filtrées/validées, vous essayez la requête et elle échoue toujours, il s'agit d'une situation exceptionnelle.

De même avec les demandes Web, vous ne pouvez pas savoir si la demande expirera ou échouera à se connecter avant d'essayer de l'envoyer. Cela justifie donc également une approche try/catch, car vous ne pouvez pas demander au serveur s'il fonctionnera quelques millisecondes plus tard lorsque vous enverrez la demande.

30
Ivan Pintar

Référence

De Le programmeur pragmatique:

Nous pensons que les exceptions devraient rarement être utilisées dans le cadre du déroulement normal d'un programme; des exceptions doivent être réservées pour les événements inattendus. Supposons qu'une exception non interceptée mettra fin à votre programme et demandez-vous: "Ce code fonctionnera-t-il toujours si je supprime tous les gestionnaires d'exceptions?" Si la réponse est "non", alors peut-être que des exceptions sont utilisées dans des circonstances non exceptionnelles.

Ils examinent ensuite l'exemple de l'ouverture d'un fichier en lecture, et le fichier n'existe pas - cela devrait-il déclencher une exception?

Si le fichier aurait dû être là, alors une exception est justifiée. [...] D'un autre côté, si vous ne savez pas si le fichier doit exister ou non, cela ne semble pas exceptionnel si vous ne le trouvez pas, et un retour d'erreur est approprié.

Plus tard, ils discutent des raisons pour lesquelles ils ont choisi cette approche:

[Une] n exception représente un transfert de contrôle immédiat et non local - c'est une sorte de goto en cascade. Les programmes qui utilisent des exceptions dans le cadre de leur traitement normal souffrent de tous les problèmes de lisibilité et de maintenabilité du code spaghetti classique. Ces programmes interrompent l'encapsulation: les routines et leurs appelants sont plus étroitement couplés via la gestion des exceptions.

Concernant votre situation

Votre question se résume à "Les erreurs de validation devraient-elles soulever des exceptions?" La réponse est que cela dépend de l'endroit où la validation a lieu.

Si la méthode en question se trouve dans une section du code où l'on suppose que les données d'entrée ont déjà été validées, les données d'entrée non valides doivent déclencher une exception; si le code est conçu de telle sorte que cette méthode reçoive la saisie exacte entrée par un utilisateur, des données non valides doivent être attendues et aucune exception ne doit être levée.

16
Mike Partridge

Il y a beaucoup de pontification philosophique ici, mais de manière générale, les conditions exceptionnelles sont simplement ces conditions que vous ne pouvez pas ou ne voulez pas gérer (autres que le nettoyage, le rapport d'erreurs, etc.) sans intervention de l'utilisateur . En d'autres termes, ce sont des conditions irrécupérables.

Si vous donnez à un programme un chemin de fichier, avec l'intention de traiter ce fichier d'une certaine manière, et que le fichier spécifié par ce chemin n'existe pas, c'est une condition exceptionnelle. Vous ne pouvez rien faire à ce sujet dans votre code, à part le signaler à l'utilisateur et lui permettre de spécifier un chemin de fichier différent.

11
Robert Harvey

Vous devez prendre en compte deux préoccupations:

  1. vous discutez d'une seule préoccupation - appelons-la Assigner car cette préoccupation est d'affecter des entrées à des objets structurés - et vous exprimez la contrainte que ses entrées soient valides

  2. a bien implémenté l'interface utilisateur a une préoccupation supplémentaire: validation des entrées utilisateur et retour constructif sur les erreurs (appelons cette partie Validator)

Du point de vue du composant Assigner, lever une exception est tout à fait raisonnable, car vous avez exprimé une contrainte qui a été violée.

Du point de vue de la expérience utilisateur, l'utilisateur ne devrait pas parler directement à ce Assigner en premier lieu. Ils devraient lui parler via le Validator.

Maintenant, dans Validator, une entrée utilisateur invalide est pas un cas exceptionnel, c'est vraiment le cas qui vous intéresse le plus. Donc, ici, une exception ne serait pas appropriée, et cela est également l'endroit où vous souhaitez identifier les erreurs toutes plutôt que de renoncer à la première.

Vous remarquerez que je n'ai pas mentionné comment ces préoccupations sont implémentées. Il semble que vous parliez du Assigner et votre collègue parle d'un combiné Validator+Assigner. Une fois que vous vous rendez compte qu'il y a sont deux préoccupations distinctes (ou séparables), vous pouvez au moins en discuter de manière judicieuse.


Pour répondre au commentaire de Renan, je suis juste en supposant qu'une fois que vous avez identifié vos deux préoccupations distinctes, il est évident quels cas doivent être considérés comme exceptionnels dans chaque contexte.

En fait, si cela n'est pas évident si quelque chose doit être considéré comme exceptionnel, je dirais que vous n'avez probablement pas fini d'identifier les problèmes indépendants dans votre solution.

Je suppose que cela fait la réponse directe à

... comment savoir si mon cas est exceptionnel?

continuez à simplifier jusqu'à ce que ce soit évident. Lorsque vous avez une pile de concepts simples que vous comprenez bien, vous pouvez raisonner clairement pour les recomposer en code, classes, bibliothèques ou autre.

7
Useless

D'autres ont bien répondu, mais voici ma courte réponse. L'exception est une situation où quelque chose dans l'environnement ne fonctionne pas, que vous ne pouvez pas contrôler et que votre code ne peut pas avancer du tout. Dans ce cas, vous devrez également informer l'utilisateur de ce qui ne va pas, pourquoi vous ne pouvez pas aller plus loin et quelle est la résolution.

4
Manoj R

Je n'ai jamais été un grand fan du conseil selon lequel vous ne devriez lever des exceptions que dans des cas exceptionnels, en partie parce qu'il ne dit rien (c'est comme dire que vous ne devriez manger que des aliments comestibles), mais aussi parce que cela est très subjectif, et on ne sait souvent pas ce qui constitue un cas exceptionnel et ce qui ne l’est pas.

Cependant, il existe de bonnes raisons à ce conseil: lancer et intercepter des exceptions est lent, et si vous exécutez votre code dans le débogueur dans Visual Studio alors qu'il est configuré pour vous avertir chaque fois qu'une exception est levée, vous pouvez finir par être spammé par des dizaines sinon des centaines de messages bien avant d'arriver au problème.

Donc, en règle générale, si:

  • votre code est exempt de bogues et
  • les services dont elle dépend sont tous disponibles, et
  • votre utilisateur utilise votre programme de la manière dont il était destiné à être utilisé (même si certaines des entrées fournies ne sont pas valides)

alors votre code ne devrait jamais lever d'exception, même celle qui est interceptée plus tard. Pour intercepter des données non valides, vous pouvez utiliser des validateurs au niveau de l'interface utilisateur ou du code tel que Int32.TryParse() dans la couche de présentation.

Pour toute autre chose, vous devez vous en tenir au principe que ne exception signifie que votre méthode ne peut pas faire ce que son nom dit qu'elle fait. En général, ce n'est pas une bonne idée d'utiliser des codes de retour pour indiquer l'échec ( à moins que le nom de votre méthode n'indique clairement qu'il le fait, par exemple TryParse()) pour deux raisons. Tout d'abord, la réponse par défaut à un code d'erreur est d'ignorer la condition d'erreur et de continuer malgré tout; deuxièmement, vous pouvez trop facilement vous retrouver avec certaines méthodes utilisant des codes retour et d'autres méthodes utilisant des exceptions, et en oubliant laquelle est laquelle. J'ai même vu des bases de code où deux implémentations interchangeables différentes de la même interface adoptent des approches différentes ici.

3
jammycakes

Il peut être nécessaire de signaler toutes les erreurs que nous pouvons trouver dans l'entrée, pas seulement la première.

C'est pourquoi vous ne pouvez pas lancer d'exception ici. Une exception interrompt immédiatement le processus de validation. Il y aurait donc beaucoup de travail à faire pour y arriver.

Un mauvais exemple:

Méthode de validation pour la classe Dog à l'aide d'exceptions:

void validate(Set<DogValidationException> previousExceptions) {
    if (!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        DogValidationException disallowedName = new DogValidationException(Problem.DISALLOWED_DOG_NAME);
        if (!previousExceptions.contains(disallowedName)){
            throw disallowedName;
        }
    }
    if (this.legs < 4) {
        DogValidationException invalidDog = new DogValidationException(Problem.LITERALLY_INVALID_DOG);
        if (!previousExceptions.contains(invalidDog)){
            throw invalidDog;
        }
    }
    // etc.
}

Comment l'appeler:

Set<DogValidationException> exceptions = new HashSet<DogValidationException>();
boolean retry;
do {
    retry = false;
    try {
        dog.validate(exceptions);
    } catch (DogValidationException e) {
        exceptions.add(e);
        retry = true;
    }
} while (retry);

if(exceptions.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}

Le problème ici est que le processus de validation, pour obtenir toutes les erreurs, nécessiterait d'ignorer les exceptions déjà trouvées. Ce qui précède pourrait fonctionner, mais il s'agit clairement d'une mauvaise utilisation des exceptions. Le type de validation qui vous a été demandé doit avoir lieu avant que la base de données soit touchée. Il n'est donc pas nécessaire d'annuler quoi que ce soit. Et, les résultats de la validation sont susceptibles d'être des erreurs de validation (espérons-le zéro, cependant).

La meilleure approche est:

Appel de méthode:

Set<Problem> validationResults = dog.validate();
if(validationResults.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}

Méthode de validation:

Set<Problem> validate() {
    Set<Problem> result = new HashSet<Problem>();
    if(!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        result.add(Problem.DISALLOWED_DOG_NAME);
    }
    if(this.legs < 4) {
        result.add(Problem.LITERALLY_INVALID_DOG);
    }
    // etc.
    return result;
}

Pourquoi? Il y a des tonnes de raisons, et la plupart des raisons ont été signalées dans les autres réponses. Pour faire simple: C'est beaucoup plus simple à lire et à comprendre par les autres. Deuxièmement, voulez-vous montrer les traces de la pile utilisateur pour lui expliquer qu'il a mal configuré son dog?

If, lors de la validation dans le deuxième exemple, toujours une erreur se produit, même si votre validateur a validé le dog avec zéro problème, alors lever une exception est la bonne chose. Comme: Aucune connexion à la base de données, l'entrée de la base de données a été modifiée par quelqu'un d'autre entre-temps, ou autre.

2
Matthias Ronge

Les exceptions devraient représenter des conditions immédiat le code appelant ne sera pas préparé à gérer, même si la méthode appelante le pourrait. Considérez, par exemple, le code qui lit certaines données d'un fichier, peut supposer légitimement que tout fichier valide se terminera par un enregistrement valide et n'est pas tenu d'extraire des informations d'un enregistrement partiel.

Si la routine de lecture des données n'utilisait pas d'exceptions mais signalait simplement si la lecture avait réussi ou non, le code appelant devrait ressembler à:

temp = dataSource.readInteger();
if (temp == null) return null;
field1 = (int)temp;
temp = dataSource.readInteger();
if (temp == null) return null;
field2 = (int)temp;
temp = dataSource.readString();
if (temp == null) return null;
field3 = temp;

etc. dépensant trois lignes de code pour chaque travail utile. En revanche, si readInteger lèvera une exception en rencontrant la fin d'un fichier, et si l'appelant peut simplement transmettre l'exception, alors le code devient:

field1 = dataSource.readInteger();
field2 = dataSource.readInteger();
field3 = dataSource.readString();

Beaucoup plus simple et plus propre, avec un accent beaucoup plus grand sur le cas où les choses fonctionnent normalement. Notez que dans les cas où l'appelant immédiat serait s'attend à gérer une condition, une méthode qui renvoie un code d'erreur sera souvent plus utile que celle qui lève une exception. Par exemple, pour totaliser tous les entiers d'un fichier:

do
{
  temp = dataSource.tryReadInteger();
  if (temp == null) break;
  total += (int)temp;
} while(true);

versus

try
{
  do
  {
    total += (int)dataSource.readInteger();
  }
  while(true);
}
catch endOfDataSourceException ex
{ // Don't do anything, since this is an expected condition (eventually)
}

Le code qui demande les nombres entiers s'attend à ce que l'un de ces appels échoue. Faire en sorte que le code utilise une boucle sans fin qui s'exécutera jusqu'à ce que cela se produise est beaucoup moins élégant que d'utiliser une méthode qui indique des échecs via sa valeur de retour.

Parce que les classes ne savent souvent pas à quelles conditions leurs clients s'attendent ou ne s'attendent pas, il est souvent utile de proposer deux versions de méthodes qui pourraient échouer de la manière attendue par certains appelants et d'autres non. Cela permettra à ces méthodes d'être utilisées proprement avec les deux types d'appelants. Notez également que même les méthodes "try" devraient lever des exceptions si des situations surviennent que l'appelant ne s'attend probablement pas. Par exemple, tryReadInteger ne doit pas lever d'exception s'il rencontre une condition de fin de fichier propre (si l'appelant ne s'y attendait pas, l'appelant aurait utilisé readInteger). D'un autre côté, il devrait probablement lever une exception si les données ne peuvent pas être lues car par ex. la clé USB qui la contenait était débranchée. Bien que de tels événements devraient toujours être reconnus comme une possibilité, il est peu probable que le code d'appel immédiat soit prêt à faire quelque chose d'utile en réponse; elle ne doit certainement pas être signalée de la même manière qu'une condition de fin de fichier.

2
supercat

La chose la plus importante dans l'écriture d'un logiciel est de le rendre lisible. Toutes les autres considérations sont secondaires, y compris la rendre efficace et la rendre correcte. S'il est lisible, le reste peut être pris en charge lors de la maintenance, et s'il n'est pas lisible, il vaut mieux le jeter. Par conséquent, vous devez lever des exceptions lorsqu'il améliore la lisibilité.

Lorsque vous écrivez un algorithme, pensez à la personne à l'avenir qui le lira. Lorsque vous arrivez à un endroit où il pourrait y avoir un problème potentiel, demandez-vous si le lecteur veut voir comment vous gérez ce problème maintenant, ou le lecteur préférerait-il simplement continuer avec l'algorithme?

J'aime penser à une recette de gâteau au chocolat. Quand il vous dit d'ajouter les œufs, il a le choix: il peut soit supposer que vous avez des œufs et poursuivre la recette, soit il peut commencer une explication sur la façon dont vous pouvez obtenir des œufs si vous n'en avez pas. Il pourrait remplir un livre entier de techniques de chasse aux poulets sauvages, tout cela pour vous aider à faire un gâteau. C'est bien, mais la plupart des gens ne voudront pas lire cette recette. La plupart des gens préfèrent simplement supposer que des œufs sont disponibles et poursuivre la recette. C'est un jugement que les auteurs doivent faire lorsqu'ils écrivent des recettes.

Il ne peut y avoir de règles garanties sur ce qui fait une bonne exception et sur les problèmes à traiter immédiatement, car cela vous oblige à lire l'esprit de votre lecteur. Le mieux que vous ferez jamais, ce sont les règles de base, et "les exceptions ne sont que pour des circonstances exceptionnelles" est assez bonne. Habituellement, quand un lecteur lit votre méthode, il cherche ce que la méthode fera 99% du temps, et il préfère ne pas avoir trop de cas bizarres comme traiter des utilisateurs entrant des entrées illégales et d'autres choses qui n'arrivent presque jamais. Ils veulent voir le flux normal de votre logiciel présenté directement, une instruction après l'autre comme si des problèmes ne se produisaient jamais. Comprendre votre programme va être assez difficile sans avoir à gérer constamment les tangentes pour résoudre tous les petits problèmes qui pourraient survenir.

2
Geo