Comment éviter de lever des exceptions vexantes?

21

La lecture de l' article d' Eric Lippert sur les exceptions a certainement été une révélation sur la façon dont je devrais aborder les exceptions, à la fois en tant que producteur et en tant que consommateur. Cependant, j'ai toujours du mal à définir une ligne directrice sur la façon d'éviter de lever des exceptions vexantes.

Plus précisément:

  • Supposons que vous ayez une méthode Save qui peut échouer car a) quelqu'un d'autre a modifié l'enregistrement avant vous , ou b) la valeur que vous essayez de créer existe déjà . Ces conditions sont à prévoir et ne sont pas exceptionnelles, donc au lieu de lever une exception, vous décidez de créer une version Try de votre méthode, TrySave, qui renvoie un booléen indiquant si la sauvegarde a réussi. Mais s'il échoue, comment le consommateur saura-t-il quel était le problème? Ou serait-il préférable de renvoyer une énumération indiquant le résultat, sorte de Ok / RecordAlreadyModified / ValueAlreadyExists? Avec integer.TryParse, ce problème n'existe pas, car il n'y a qu'une seule raison pour laquelle la méthode peut échouer.
  • L'exemple précédent est-il vraiment une situation épineuse? Ou est-ce que la levée d'une exception dans ce cas serait la meilleure façon? Je sais que c'est ainsi que cela se fait dans la plupart des bibliothèques et des cadres, y compris le cadre Entity.
  • Comment décidez-vous quand créer une version Try de votre méthode par rapport à fournir un moyen de tester à l'avance si la méthode fonctionnera ou non? Je suis actuellement en train de suivre ces directives:
    • S'il existe un risque de condition de concurrence, créez une version d'essai. Cela évite au consommateur de devoir attraper une exception exogène. Par exemple, dans la méthode Save décrite précédemment.
    • Si la méthode pour tester la condition ferait à peu près tout ce que fait la méthode d'origine, créez une version d'essai. Par exemple, integer.TryParse ().
    • Dans tous les autres cas, créez une méthode pour tester la condition.
Mike
la source
1
Votre exemple d'une sauvegarde qui peut échouer n'est pas vraiment une exception terriblement vexante. C'est assez ordinaire et devrait probablement être une exception.
S.Lott
@ S.Lott: Que voulez-vous dire par c'est assez ordinaire? La situation elle-même, ou jeter une exception dans cette situation? Quoi qu'il en soit, je suis d'accord avec vous qu'il n'est pas évident qu'il s'agisse en fait d'une situation épineuse. Je mettrai à jour la question.
Mike
"La situation elle-même, ou jeter une exception dans cette situation" Les deux.
S.Lott

Réponses:

24

Supposons que vous ayez une méthode Save qui peut échouer car a) quelqu'un d'autre a modifié l'enregistrement avant vous, ou b) la valeur que vous essayez de créer existe déjà. Ces conditions sont à prévoir et non exceptionnelles. Au lieu de lever une exception, vous décidez de créer une version Try de votre méthode, TrySave, qui renvoie un booléen indiquant si la sauvegarde a réussi. Mais s'il échoue, comment le consommateur saura-t-il quel était le problème?

Bonne question.

La première question qui me vient à l'esprit est: si les données sont déjà là, dans quel sens la sauvegarde a- t-elle échoué ? Il me semble que cela m'a réussi. Mais supposons pour le bien de l'argument que vous avez vraiment de nombreuses raisons différentes pour lesquelles une opération peut échouer.

La deuxième question qui me vient à l'esprit est: les informations que vous souhaitez renvoyer à l'utilisateur sont-elles exploitables ? Autrement dit, vont-ils prendre une décision sur la base de ces informations?

Lorsque le voyant "vérifier le moteur" s'allume, j'ouvre le capot, vérifie qu'il y a un moteur dans ma voiture qui n'est pas en feu et je l'emmène au garage. Bien sûr, au garage, ils ont toutes sortes d'équipements de diagnostic à usage spécial qui leur expliquent pourquoi le voyant du moteur de contrôle est allumé, mais de mon point de vue, le système d'avertissement est bien conçu. Je me fiche que le problème soit dû au fait que le capteur d'oxygène enregistre un niveau anormal d'oxygène dans la chambre de combustion, ou parce que le détecteur de ralenti est débranché, ou autre chose. Je vais prendre la même action, à savoir, laisser quelqu'un d'autre comprendre cela .

L'appelant se demande-t-il pourquoi la sauvegarde a échoué? Vont-ils faire quelque chose, à part abandonner ou réessayer?

Supposons pour les besoins de l'argument que l'appelant va vraiment prendre différentes actions en fonction de la raison pour laquelle l'opération a échoué.

La troisième question qui me vient à l'esprit est: le mode de défaillance est-il exceptionnel ? Je pense que vous pourriez confondre possible avec irréprochable . Je penserais à deux utilisateurs tentant de modifier le même enregistrement en même temps qu'une situation exceptionnelle mais possible, pas une situation courante.

Supposons, pour les besoins de l'argument, que ce n'est pas exceptionnel.

La quatrième question qui me vient à l'esprit est: existe-t-il un moyen de détecter de manière fiable la mauvaise situation à l'avance?

Si la mauvaise situation est dans mon seau "exogène", alors non. Il n'y a aucun moyen de dire de manière fiable "un autre utilisateur a-t-il modifié cet enregistrement?" car ils pourraient le modifier après avoir posé la question . La réponse est périmée dès qu'elle est produite.

La cinquième question qui me vient à l'esprit est: existe-t-il un moyen de concevoir l'API de manière à éviter la mauvaise situation?

Par exemple, vous pouvez faire en sorte que l'opération "enregistrer" nécessite deux étapes. Première étape: acquérir un verrou sur l'enregistrement en cours de modification. Cette opération réussit ou échoue et peut donc renvoyer un booléen. L'appelant peut alors avoir une politique sur la façon de traiter l'échec: attendez un moment et réessayez, abandonnez, peu importe. Deuxième étape: une fois le verrou acquis, effectuez la sauvegarde et relâchez le verrou. Maintenant, la sauvegarde réussit toujours et il n'y a donc pas besoin de s'inquiéter de tout type de gestion des erreurs. Si la sauvegarde échoue, c'est vraiment exceptionnel.

Eric Lippert
la source
Tous de très bons points, merci. Maintenant, voici une question rhétorique qui résume probablement mon message: Si vous deviez repenser File.Open (), créeriez-vous un File.TryOpen () à la place? Comment communiqueriez-vous au consommateur la raison de l'échec? Ou lancer une exception exogène est-il vraiment le meilleur compromis ici?
Mike
10
@Mike: Les systèmes de fichiers sont un bon exemple de l'utilisation d'exceptions exogènes. Ils échouent rarement, donc l'échec est exceptionnel. Ils échouent de manière imprévisible et pour des raisons totalement indépendantes de la volonté de l'appelant (il n'y a pas de «verrou» que vous pouvez prendre qui maintient le câble Ethernet branché), et les échecs sont à la fois divers et exploitables (c'est-à-dire, un échec parce que le fichier est not found vs file is found but you have not write access are both actionable in different ways.) Toutes ces raisons représentent un échec à titre d'exception.
Eric Lippert
Je considère que la question est résolue maintenant, mais je suis juste curieux;) Si la méthode peut échouer pour 2 raisons ou plus, les échecs sont exploitables, les échecs ne sont pas exceptionnels et les échecs ne peuvent pas être détectés à l'avance ni empêchés, Qu'est-ce que tu ferais?
Mike
@ Mike Je ne peux pas parler pour Eric, mais cela semble être un bon endroit pour les codes d'erreur. Retourner un membre d'une énumération, peut-être.
Matthew Read
1

Dans votre exemple, si la situation ValueAlreadyExists peut être facilement vérifiée, elle doit être vérifiée et une exception peut être déclenchée avant de tenter l'enregistrement, je ne pense pas qu'un essai devrait être nécessaire dans cette situation. Les conditions de concurrence sont plus difficiles à vérifier à l'avance, donc envelopper le Save in a Try dans ce cas est probablement une très bonne idée.

En général, s'il y a une condition qui je pense est très probable (comme NoDataReturned, DivideByZero, etc ...) OU est très facile à vérifier (comme une collection vide ou une valeur NULL), j'essaie de vérifier à l'avance et à y faire face avant même que j'arrive au point où je devrais attraper une exception. J'avoue qu'il n'est pas toujours facile de connaître ces conditions à l'avance, parfois elles n'apparaissent que lorsque le code est soumis à des tests rigoureux.

FrustratedWithFormsDesigner
la source
0

La save()méthode doit lever une exception.

La couche la plus haute doit intercepter et informer l'utilisateur , sans arrêter le programme, sauf s'il s'agit d'un programme en ligne de commande de type Unix, auquel cas il est OK de terminer.

Les valeurs de retour ne sont pas un bon moyen de gérer les exceptions.

Tulains Córdova
la source