web-dev-qa-db-fra.com

Les variables d'erreur sont-elles un anti-modèle ou une bonne conception?

Afin de gérer plusieurs erreurs possibles qui ne devraient pas arrêter l'exécution, j'ai une variable error que les clients peuvent vérifier et utiliser pour lever des exceptions. Est-ce un anti-modèle? Y a-t-il une meilleure façon de gérer cela? Pour un exemple de cela en action, vous pouvez voir l'API mysqli de PHP. Supposons que les problèmes de visibilité (accesseurs, portée publique et privée, la variable dans une classe ou globale?) Sont traités correctement.

45
Mikayla Maki

Si un langage prend en charge les exceptions de manière inhérente, il est préférable de lever des exceptions et les clients peuvent intercepter l'exception s'ils ne veulent pas entraîner un échec. En fait, les clients de votre code attendent des exceptions et rencontreront de nombreux bogues car ils ne vérifieront pas les valeurs de retour.

Si vous avez le choix, l'utilisation d'exceptions présente de nombreux avantages.

Messages

Les exceptions contiennent des messages d'erreur lisibles par l'utilisateur qui peuvent être utilisés par les développeurs pour le débogage ou même affichés aux utilisateurs s'ils le souhaitent. Si le code consommateur ne peut pas gérer l'exception, il peut toujours le journaliser afin que les développeurs puissent parcourir les journaux sans avoir à s'arrêter à chaque autre trace pour déterminer quelle était la valeur de retour et la mapper dans un tableau pour déterminer quelle était la valeur. exception réelle.

Avec les valeurs de retour, aucune information supplémentaire ne peut être facilement fournie. Certaines langues prendront en charge les appels de méthode pour obtenir le dernier message d'erreur, donc cette préoccupation est un peu apaisée, mais cela nécessite que l'appelant fasse des appels supplémentaires et nécessitera parfois l'accès à un `` objet spécial '' qui transporte ces informations.

Dans le cas des messages d'exception, je fournis autant de contexte que possible, tels que:

Une stratégie de nom "foo" n'a pas pu être récupérée pour l'utilisateur "bar", qui était référencé dans le profil de l'utilisateur.

Comparez cela à un code retour -85. Lequel préfèrerais tu?

Piles d'appels

Les exceptions ont généralement également des piles d'appels détaillées qui aident à déboguer le code plus rapidement et plus rapidement, et peuvent également être enregistrées par le code appelant si vous le souhaitez. Cela permet aux développeurs de localiser le problème généralement sur la ligne exacte, et est donc très puissant. Encore une fois, comparez cela à un fichier journal avec des valeurs de retour (comme un -85, 101, 0, etc.), lequel préférez-vous?

échec de l'approche biaisée rapide

Si une méthode est appelée quelque part qui échoue, elle lèvera une exception. Le code appelant doit supprimer explicitement l'exception ou il échouera. J'ai trouvé cela vraiment incroyable car pendant le développement et les tests (et même en production) le code échoue rapidement, forçant les développeurs à le corriger. Dans le cas des valeurs de retour, si une vérification d'une valeur de retour est manquée, l'erreur est silencieusement ignorée et le bogue fait surface quelque part inattendu, généralement avec un coût beaucoup plus élevé à déboguer et à corriger.

Exceptions d'habillage et de déballage

Les exceptions peuvent être encapsulées dans d'autres exceptions, puis dépliées si nécessaire. Par exemple, votre code peut jeter ArgumentNullException que le code appelant peut encapsuler dans un UnableToRetrievePolicyException car cette opération a échoué dans le code appelant. Bien que l'utilisateur puisse afficher un message similaire à l'exemple que j'ai fourni ci-dessus, un code de diagnostic peut déballer l'exception et constater qu'un ArgumentNullException a causé le problème, ce qui signifie qu'il s'agit d'une erreur de codage dans le code de votre consommateur. Cela pourrait alors déclencher une alerte afin que le développeur puisse corriger le code. Ces scénarios avancés ne sont pas faciles à implémenter avec les valeurs de retour.

Simplicité du code

Celui-ci est un peu plus difficile à expliquer, mais j'ai appris grâce à ce codage à la fois avec des valeurs de retour et des exceptions. Le code qui a été écrit à l'aide de valeurs de retour effectue généralement un appel, puis effectue une série de vérifications sur la valeur de retour. Dans certains cas, il ferait appel à une autre méthode et aura maintenant une autre série de vérifications pour les valeurs de retour de cette méthode. Avec les exceptions, la gestion des exceptions est beaucoup plus simple dans la plupart sinon tous les cas. Vous avez un bloc try/catch/finally, le runtime essayant de son mieux d'exécuter le code dans les blocs finally pour le nettoyage. Même les blocs imbriqués try/catch/finally sont relativement plus faciles à suivre et à maintenir que les blocs imbriqués if/else et les valeurs de retour associées provenant de plusieurs méthodes.

Conclusion

Si la plate-forme que vous utilisez prend en charge les exceptions (en particulier comme Java ou .NET), alors vous devez certainement supposer qu'il n'y a pas d'autre moyen que de lever des exceptions car ces plates-formes ont des directives à lancer les exceptions, et vos clients s’y attendront. Si j’utilisais votre bibliothèque, je ne prendrai pas la peine de vérifier les valeurs de retour car je m'attends à ce que des exceptions soient levées, c'est comme ça que le monde dans ces plateformes est.

Cependant, s'il s'agissait de C++, il serait un peu plus difficile à déterminer car une grande base de code existe déjà avec des codes de retour, et un grand nombre de développeurs sont réglés pour renvoyer des valeurs par opposition aux exceptions (par exemple, Windows regorge de HRESULT) . De plus, dans de nombreuses applications, cela peut également être un problème de performance (ou du moins perçu comme tel).

66
Omer Iqbal

Il existe plusieurs façons de signaler une erreur:

  • une variable d'erreur à vérifier: C , Go , ...
  • une exception: Java , C # , ...
  • un gestionnaire de "condition": LISP (seulement?), ...
  • un retour polymorphe: Haskell , ML , Rust , ...

Le problème de la variable d'erreur est qu'il est facile d'oublier de vérifier.

Le problème des exceptions est qu'il crée des chemins cachés d'exécutions, et, bien que try/catch soit facile à écrire, assurer une récupération correcte dans la clause catch est vraiment difficile à retirer (pas de support de type systèmes/compilateurs).

Le problème des gestionnaires de conditions est qu'ils ne se composent pas bien: si vous avez une exécution de code dynamique (fonctions virtuelles), il est impossible de prédire quelles conditions doivent être gérées. De plus, si la même condition peut être soulevée à plusieurs endroits, rien ne dit qu'une solution uniforme peut être appliquée à chaque fois, et elle devient rapidement désordonnée.

Retours polymorphes (Either a b à Haskell) sont ma solution préférée jusqu'à présent:

  • explicite: pas de chemin d'exécution caché
  • explicite: entièrement documenté dans la signature du type de fonction (pas de surprise)
  • difficile à ignorer: vous devez faire correspondre les motifs pour obtenir le résultat souhaité et gérer le cas d'erreur

Le seul problème est qu'ils peuvent potentiellement conduire à une vérification excessive; les langages qui les utilisent ont des idiomes pour enchaîner les appels de fonctions qui les utilisent, mais cela peut encore nécessiter un peu plus de frappe/encombrement. Dans Haskell, ce serait monades ; cependant, c'est beaucoup plus effrayant qu'il n'y paraît, voir Railway Oriented Programming.

21
Matthieu M.

Les variables d'erreur sont une relique de langages comme C, où les exceptions n'étaient pas disponibles. Aujourd'hui, vous devez les éviter, sauf lorsque vous écrivez une bibliothèque qui est potentiellement utilisée à partir d'un programme C (ou d'un langage similaire sans gestion des exceptions).

Bien sûr, si vous avez un type d'erreur qui pourrait être mieux classé comme "avertissement" (= votre bibliothèque peut fournir un résultat valide et l'appelant peut ignorer l'avertissement s'il pense que ce n'est pas important), alors un indicateur d'état sous forme d'une variable peut avoir un sens même dans les langues avec des exceptions. Mais méfiez-vous. Les appelants de la bibliothèque ont tendance à ignorer ces avertissements même s'ils ne le devraient pas. Réfléchissez donc à deux fois avant d'introduire une telle construction dans votre bibliothèque.

21
Doc Brown

Je pense que c'est affreux. Je suis actuellement en train de refactoriser une application Java qui utilise des valeurs de retour au lieu d'exceptions. Bien que vous ne puissiez pas du tout travailler avec Java, je pense que cela s'applique néanmoins.

Vous vous retrouvez avec un code comme celui-ci:

String result = x.doActionA();
if (result != null) {
  throw new Exception(result);
}
result = x.doActionB();
if (result != null) {
  throw new Exception(result);
}

Ou ca:

if (!x.doActionA()) {
  throw new Exception(x.getError());
}
if (!x.doActionB()) {
  throw new Exception(x.getError());
}

Je préfère de beaucoup que les actions lèvent elles-mêmes des exceptions, donc vous vous retrouvez avec quelque chose comme:

x.doActionA();
x.doActionB();

Vous pouvez envelopper cela dans un essai et obtenir le message de l'exception, ou vous pouvez choisir d'ignorer l'exception, par exemple lorsque vous supprimez quelque chose qui a peut-être déjà disparu. Il conserve également votre trace de pile, si vous en avez une. Les méthodes elles-mêmes deviennent également plus faciles. Au lieu de gérer les exceptions elles-mêmes, ils jettent simplement ce qui a mal tourné.

Code actuel (horrible):

private String doActionA() {
  try {
    someOperationThatCanGoWrong1();
    someOperationThatCanGoWrong2();
    someOperationThatCanGoWrong3();
    return null;
  } catch(Exception e) {
    return "Something went wrong!";
  }
}

Nouveau et amélioré:

private void doActionA() throws Exception {
  someOperationThatCanGoWrong1();
  someOperationThatCanGoWrong2();
  someOperationThatCanGoWrong3();
}

La trace de trace est conservée et le message est disponible dans l'exception, plutôt que l'inutile "Quelque chose s'est mal passé!".

Vous pouvez, bien sûr, fournir de meilleurs messages d'erreur, et vous devriez le faire. Mais ce post est ici parce que le code actuel sur lequel je travaille est une douleur, et vous ne devriez pas faire de même.

12
mrjink

Il n'y a souvent rien de mal à utiliser ce modèle ou ce modèle, tant que vous utilisez le modèle que tout le monde utilise. Dans le développement Objective-C , le modèle préféré est de passer un pointeur où la méthode appelée peut déposer un objet NSError. Des exceptions sont réservées aux erreurs de programmation et entraînent un plantage (sauf si vous avez des programmeurs Java ou . NET écrivant leur première application iPhone). Et cela fonctionne très bien.

5
gnasher729

"Afin de gérer plusieurs erreurs possibles, cela ne devrait pas interrompre l'exécution",

Si vous voulez dire que les erreurs ne doivent pas arrêter l'exécution de la fonction actuelle, mais doivent être signalées à l'appelant d'une manière ou d'une autre - alors vous avez quelques options qui n'ont pas vraiment été mentionnées. Ce cas est vraiment plus un avertissement qu'une erreur. Lancer/retourner n'est pas une option car cela met fin à la fonction actuelle. Un seul paramètre ou retour de message d'erreur ne permet qu'au maximum une de ces erreurs de se produire.

J'ai utilisé deux modèles:

  • Une collection d'erreurs/avertissements, transmise ou conservée en tant que variable membre. À laquelle vous ajoutez des éléments et continuez à traiter. Personnellement, je n'aime pas vraiment cette approche car je pense qu'elle affaiblit l'appelant.

  • Transmettez un objet gestionnaire d'erreur/d'avertissement (ou définissez-le comme variable membre). Et chaque erreur appelle une fonction membre du gestionnaire. De cette façon, l'appelant peut décider quoi faire avec de telles erreurs sans fin.

Ce que vous transmettez à ces collections/gestionnaires doit contenir suffisamment de contexte pour que l'erreur soit gérée "correctement" - Une chaîne est généralement trop petite, il est souvent judicieux de lui transmettre une instance d'Exception - mais parfois mal vue (comme un abus d'exceptions) .

Le code typique utilisant un gestionnaire d'erreurs pourrait ressembler à ceci

class MyFunClass {
  public interface ErrorHandler {
     void onError(Exception e);
     void onWarning(Exception e);
  }

  ErrorHandler eh;

  public void canFail(int i) {
     if(i==0) {
        if(eh!=null) eh.onWarning(new Exception("canFail shouldn't be called with i=0"));
     }
     if(i==1) {
        if(eh!=null) eh.onError(new Exception("canFail called with i=1 is fatal");
        throw new RuntimeException("canFail called with i=2");
     }
     if(i==2) {
        if(eh!=null) eh.onError(new Exception("canFail called with i=2 is an error, but not fatal"));
     }
  }
}
5
Michael Anderson

On a déjà répondu à la question, mais je ne peux pas m'en empêcher.

Vous ne pouvez pas vraiment vous attendre à ce qu'Exception fournisse une solution pour tous les cas d'utilisation. Marteler quelqu'un?

Il y a des cas où les exceptions ne sont pas toutes finales et sont toutes, par exemple, si une méthode reçoit une demande et est responsable de valider tous les champs passés, et pas seulement le premier, alors vous devez penser qu'il devrait être possible de indiquer la cause de l'erreur pour plusieurs champs. Il devrait être possible d'indiquer également si la nature de la validation empêche ou non l'utilisateur d'aller plus loin. Un exemple de cela serait un mot de passe pas fort. Vous pouvez afficher un message à l'utilisateur indiquant que le mot de passe entré n'est pas très fort, mais qu'il est assez fort.

Vous pourriez faire valoir que toutes ces validations pourraient être levées comme exception à la fin du module de validation, mais ce seraient des codes d'erreur dans tout sauf dans le nom.

La leçon à retenir est donc la suivante: les exceptions ont leur place, tout comme les codes d'erreur. Choisissez judicieusement.

4
Alexandre Santos

Il existe des cas d'utilisation où les codes d'erreur sont préférables aux exceptions.

Si votre code peut continuer malgré l'erreur, mais qu'il doit être signalé, une exception est un mauvais choix car les exceptions mettent fin au flux. Par exemple, si vous lisez un fichier de données et découvrez qu'il contient des données incorrectes non terminales, il peut être préférable de lire le reste du fichier et de signaler l'erreur plutôt que d'échouer.

Les autres réponses ont expliqué pourquoi les exceptions devraient être préférées aux codes d'erreur en général.

4
Jack Aidley

Il n'y a certainement rien de mal à ne pas utiliser d'exceptions lorsque les exceptions ne conviennent pas.

Lorsque l'exécution du code ne doit pas être interrompue (par exemple, en agissant sur les entrées utilisateur pouvant contenir plusieurs erreurs, comme un programme à compiler ou un formulaire à traiter), je trouve que la collecte des erreurs dans des variables d'erreur comme has_errors et error_messages est en effet un design bien plus élégant que de lever une exception à la première erreur. Il permet de trouver toutes les erreurs dans les entrées utilisateur sans forcer l'utilisateur à soumettre de nouveau inutilement.

2
ikh

Dans certains langages de programmation dynamiques, vous pouvez utiliser à la fois valeurs d'erreur et gestion des exceptions. Cela se fait en retournant un objet d'exception non retourné à la place de la valeur de retour ordinaire, qui peut être vérifiée comme une valeur d'erreur, mais il lève une exception s'il ne l'est pas pas vérifié.

Dans Perl 6 , cela se fait via fail, qui en cas de no fatal; scope renvoie un objet d'exception Failure spécial non retourné.

Dans Perl 5 vous pouvez utiliser Contextual :: Return vous pouvez le faire avec return FAIL.

1
Jakub Narębski