Exceptions - «ce qui s'est passé» vs «que faire»

19

Nous utilisons des exceptions pour permettre au consommateur du code de gérer un comportement inattendu de manière utile. Habituellement, des exceptions sont construites autour du scénario "ce qui s'est passé" - comme FileNotFound(nous n'avons pas pu trouver le fichier que vous avez spécifié) ou ZeroDivisionError(nous n'avons pas pu effectuer l' 1/0opération).

Et s'il est possible de spécifier le comportement attendu du consommateur?

Par exemple, imaginez que nous avons une fetchressource, qui exécute la requête HTTP et renvoie les données récupérées. Et au lieu d'erreurs comme ServiceTemporaryUnavailableou RateLimitExceedednous soulèverions simplement un RetryableErrorsuggérant au consommateur qu'il devrait simplement réessayer la demande et ne pas se soucier d'un échec spécifique. Donc, nous suggérons essentiellement une action à l'appelant - le "quoi faire".

Nous ne le faisons pas souvent parce que nous ne connaissons pas toutes les utilisations des consommateurs. Mais imaginez que c'est un composant spécifique que nous connaissons la meilleure ligne de conduite pour un appelant - alors devrions-nous alors utiliser l'approche «que faire»?

Roman Bodnarchuk
la source
3
HTTP ne fait-il pas déjà cela? 503 est un échec temporaire de réponse, donc le demandeur doit réessayer, 404 est une absence fondamentale, donc cela n'a aucun sens de réessayer, 301 signifie "déplacé de façon permanente", vous devez donc réessayer, mais avec une adresse différente, etc.
Kilian Le
7
Dans de nombreux cas, si nous savons vraiment "quoi faire", nous pouvons simplement faire en sorte que l'ordinateur le fasse automatiquement et l'utilisateur n'a même pas besoin de savoir que quelque chose s'est mal passé. Je suppose que chaque fois que mon navigateur reçoit un 301, il va simplement à la nouvelle adresse sans me le demander.
Ixrec
@Ixrec - a eu la même idée aussi. cependant, le consommateur peut ne pas vouloir attendre une autre demande et ignorer l'article ou échouer complètement.
Roman Bodnarchuk
1
@RomanBodnarchuk: Je ne suis pas d'accord. C'est comme dire qu'une personne ne devrait pas avoir besoin de connaître le chinois pour parler chinois. HTTP est un protocole, et le client et le serveur sont censés le connaître et le suivre. Voilà comment fonctionne un protocole. Si un seul côté le connaît et le respecte, alors vous ne pouvez pas communiquer.
Chris Pratt
1
Cela ressemble honnêtement à ce que vous essayez de remplacer vos exceptions par un bloc catch. C'est pourquoi nous sommes passés aux exceptions - pas plus if( ! function() ) handle_something();, mais pouvoir gérer l'erreur quelque part où vous connaissez réellement le contexte d'appel - c'est-à-dire dire à un client d'appeler un administrateur sys si votre serveur a échoué ou de se recharger automatiquement si la connexion a chuté, mais vous alerter dans cas, l'appelant est un autre microservice. Laissez les blocs de capture gérer la capture.
Sebb

Réponses:

47

Mais imaginez que c'est un composant spécifique que nous connaissons le meilleur plan d'action pour un appelant.

Cela échoue presque toujours pour au moins l'un de vos appelants, pour lequel ce comportement est extrêmement irritant. Ne présumez pas que vous connaissez le mieux. Dites à vos utilisateurs ce qui se passe, pas ce que vous supposez qu'ils devraient faire à ce sujet. Dans de nombreux cas, il est déjà clair ce que devrait être un plan d'action sensé (et, si ce n'est pas le cas, faites une suggestion dans votre manuel d'utilisation).

Par exemple, même les exceptions données dans votre question démontrent votre hypothèse cassée: a ServiceTemporaryUnavailableéquivaut à "réessayer plus tard" et RateLimitExceededéquivaut à "woah there chill out, peut-être ajuster vos paramètres de minuterie et réessayer dans quelques minutes". Mais l'utilisateur peut aussi vouloir déclencher une sorte d'alarme ServiceTemporaryUnavailable(ce qui indique un problème de serveur), et pas pour RateLimitExceeded(ce qui n'est pas le cas).

Donnez-leur le choix .

Courses de légèreté avec Monica
la source
4
Je suis d'accord. Le serveur ne doit transmettre les informations que correctement. D'autre part, la documentation devrait clairement décrire la ligne de conduite appropriée dans de tels cas.
Neil
1
Un petit défaut dans cela est que pour certains pirates, certaines exceptions peuvent leur en dire beaucoup sur ce que fait votre code et ils peuvent l'utiliser pour trouver des moyens de l'exploiter.
Pharap
3
@Pharap Si vos pirates ont accès à l'exception elle-même au lieu d'un message d'erreur, vous êtes déjà perdu.
corsiKa
2
J'aime cette réponse, mais il manque ce que je considère comme une exigence d'exceptions ... si vous saviez comment récupérer, ce ne serait pas une exception! Une exception ne devrait être que lorsque vous ne pouvez pas faire quelque chose: entrée non valide, état non valide, sécurité non valide - vous ne pouvez pas les corriger par programme.
corsiKa
1
D'accord. Si vous insistez pour indiquer si une nouvelle tentative est possible, vous pouvez toujours simplement hériter des exceptions concrètes pertinentes RetryableError.
sapi
18

Avertissement! Un programmeur C ++ arrive ici avec des idées peut-être différentes sur la façon de gérer les exceptions en essayant de répondre à une question qui concerne certainement un autre langage!

Compte tenu de cette idée:

Par exemple, imaginez que nous avons une ressource d'extraction, qui exécute une requête HTTP et retourne les données récupérées. Et au lieu d'erreurs comme ServiceTemporaryUnavailable ou RateLimitExceeded, nous soulèverions simplement une RetryableError suggérant au consommateur qu'il devrait simplement réessayer la demande et ne pas se soucier d'un échec spécifique.

... une chose que je suggérerais est que vous pourriez mélanger les préoccupations de signaler une erreur avec des plans d'action pour y répondre d'une manière qui pourrait dégrader la généralité de votre code ou nécessiter beaucoup de "points de traduction" pour les exceptions .

Par exemple, si je modélise une transaction impliquant le chargement d'un fichier, elle peut échouer pour plusieurs raisons. Peut-être que le chargement du fichier implique le chargement d'un plugin qui n'existe pas sur la machine de l'utilisateur. Le fichier est peut-être simplement corrompu et nous avons rencontré une erreur lors de son analyse.

Peu importe ce qui se passe, disons que la procédure consiste à signaler ce qui est arrivé à l'utilisateur et à lui demander ce qu'il veut faire ("réessayer, charger un autre fichier, annuler").

Lanceur contre receveur

Cette ligne de conduite s'applique quel que soit le type d'erreur que nous avons rencontré dans ce cas. Il n'est pas intégré à l'idée générale d'une erreur d'analyse, il n'est pas intégré à l'idée générale de l'échec du chargement d'un plugin. Il est intégré à l'idée de rencontrer de telles erreurs lors du contexte précis de chargement d'un fichier (combinaison de chargement d'un fichier et d'échec). Donc, généralement, je le vois, grossièrement parlant, comme la catcher'sresponsabilité de déterminer le plan d'action en réponse à une exception levée (ex: inviter l'utilisateur avec des options), pas le thrower's.

Autrement dit, les sites auxquels les throwexceptions manquent généralement ce type d'informations contextuelles, surtout si les fonctions qui lancent sont généralement applicables. Même dans un contexte totalement dégénéré quand ils ont ces informations, vous vous retrouvez coincé en termes de comportement de récupération en l'intégrant dans le throwsite. Les sites qui catchsont généralement ceux qui ont le plus d'informations disponibles pour déterminer une ligne de conduite, et vous donnent un endroit central pour modifier si cette ligne de conduite doit jamais changer pour cette transaction donnée.

Lorsque vous commencez à essayer de lever des exceptions ne signalant plus ce qui ne va pas mais essayant de déterminer quoi faire, cela pourrait dégrader la généralité et la flexibilité de votre code. Une erreur d'analyse ne doit pas toujours conduire à ce type d'invite, elle varie en fonction du contexte dans lequel une telle exception est levée (la transaction dans laquelle elle a été levée).

Le lanceur aveugle

De manière générale, une grande partie de la conception de la gestion des exceptions tourne souvent autour de l'idée d'un lanceur aveugle. Il ne sait pas comment l'exception va être interceptée, ni où. Il en va de même pour les anciennes formes de récupération d'erreurs utilisant la propagation d'erreur manuelle. Les sites qui rencontrent des erreurs n'incluent pas de ligne de conduite utilisateur, ils incorporent uniquement les informations minimales pour signaler le type d'erreur rencontré.

Responsabilités inversées et généralisation du receveur

En y réfléchissant plus attentivement, j'essayais d'imaginer le type de base de code où cela pourrait devenir une tentation. Mon imagination (peut-être erronée) est que votre équipe joue toujours le rôle de "consommateur" ici et implémente également la plupart du code d'appel. Peut-être que vous avez beaucoup de transactions disparates (beaucoup de tryblocs) qui peuvent toutes se heurter aux mêmes ensembles d'erreurs, et toutes devraient, du point de vue de la conception, conduire à une action uniforme de récupération.

En tenant compte des conseils judicieux de la Lightness Races in Orbit'sbonne réponse (qui, je pense, vient vraiment d'un état d'esprit avancé orienté bibliothèque), vous pourriez toujours être tenté de lever des exceptions "que faire", seulement plus près du site de récupération de transaction.

Il pourrait être possible de trouver ici un site intermédiaire et commun de traitement des transactions qui centralise réellement les préoccupations "que faire" mais toujours dans le contexte de la capture.

entrez la description de l'image ici

Cela ne s'appliquerait que si vous pouvez concevoir une sorte de fonction générale que toutes ces transactions externes utilisent (ex: une fonction qui entre une autre fonction à appeler ou une classe de base de transaction abstraite avec un comportement remplaçable modélisant ce site de transaction intermédiaire qui effectue la capture sophistiquée ).

Pourtant, celui-ci pourrait être responsable de la centralisation du plan d'action de l'utilisateur en réponse à une variété d'erreurs possibles, et toujours dans le contexte de la capture plutôt que du lancer. Exemple simple (pseudocode Python-ish, et je ne suis pas du tout un développeur Python expérimenté, donc il pourrait y avoir une façon plus idiomatique de procéder):

def general_catcher(task):
    try:
       task()
    except SomeError1:
       # do some uniformly-designed recovery stuff here
    except SomeError2:
       # do some other uniformly-designed recovery stuff here
    ...

[Avec un peu de chance avec un meilleur nom que general_catcher]. Dans cet exemple, vous pouvez transmettre une fonction contenant la tâche à exécuter tout en bénéficiant d'un comportement de capture généralisé / unifié pour tous les types d'exceptions qui vous intéressent, et continuer à étendre ou modifier la partie «que faire» tout vous aimez depuis cet emplacement central et toujours dans un catchcontexte où cela est généralement encouragé. Mieux encore, nous pouvons empêcher les sites de lancer de se préoccuper de "quoi faire" (en préservant la notion de "lanceur aveugle").

Si vous ne trouvez aucune de ces suggestions ici utile et qu'il y a une forte tentation de lever des exceptions "quoi faire" de toute façon, sachez surtout que cela est très anti-idiomatique à tout le moins, ainsi que potentiellement décourageant un état d'esprit généralisé.

ChrisF
la source
2
+1. Je n'ai jamais entendu parler de l'idée de "Blind Thrower" en tant que telle auparavant, mais elle correspond à la façon dont je pense à la gestion des exceptions: indiquez que l'erreur s'est produite, ne pensez pas à la façon dont elle devrait être gérée. Lorsque vous êtes responsable de la pile complète, il est difficile (mais important!) De séparer les responsabilités proprement, et l'appelé est responsable de la notification des problèmes et de l'appelant pour la gestion des problèmes. L'appelé sait seulement ce qu'on lui a demandé de faire, pas pourquoi. La gestion des erreurs doit se faire dans le contexte du «pourquoi», donc: dans l'appelant.
Sjoerd Job Postmus
1
(Aussi: je ne pense pas que votre réponse soit spécifique au C ++, mais applicable à la gestion des exceptions en général)
Sjoerd Job Postmus
1
@SjoerdJobPostmus Oui merci! Le "Blind Thrower" est juste une analogie maladroite que j'ai trouvée ici - je ne suis pas très intelligent ou rapide pour digérer des concepts techniques complexes, donc je veux souvent trouver peu d'images et d'analogies pour essayer d'expliquer et d'améliorer ma propre compréhension de choses. Peut-être qu'un jour je pourrai essayer de m'écrire un petit livre de programmation rempli de nombreux dessins animés. :-D
1
Hé, c'est une jolie petite image. Un petit personnage de bande dessinée portant un bandeau sur les yeux et jetant des exceptions en forme de baseball, ne sachant pas qui les rattrapera (ou même s'ils seront attrapés), mais remplissant son devoir de lanceur de blinds.
Blacklight Shining
1
@DrunkCoder: Veuillez ne pas vandaliser vos messages. Nous avons déjà suffisamment de choses borken sur Internet. Si vous avez une bonne raison de supprimer, signalez votre message à l'attention du modérateur et faites valoir votre point de vue.
Robert Harvey
2

Je pense que la plupart du temps, il serait préférable de passer des arguments à la fonction pour lui dire comment gérer ces situations.

Par exemple, considérons une fonction:

Response fetchUrl(URL url, RetryPolicy retryPolicy);

Je peux passer RetryPolicy.noRetries () ou RetryPolicy.retries (3) ou autre chose. En cas d'échec d'une nouvelle tentative, il consultera la politique pour décider s'il doit ou non réessayer.

Winston Ewert
la source
Cela n'a cependant pas grand-chose à voir avec le renvoi d'exceptions au site d'appel. Vous parlez de quelque chose d'un peu différent, ce qui est bien mais ne fait pas vraiment partie de la question ..
Courses de légèreté avec Monica
@LightnessRacesinOrbit, au contraire. Je le présente comme une alternative à l'idée de renvoyer des exceptions au site d'appel. Dans l'exemple de l'OP, fetchUrl lèverait une RetryableException, et je dis plutôt que vous devriez dire à fetchUrl quand il doit réessayer.
Winston Ewert
2
@WinstonEwert: Bien que je sois d'accord avec LightnessRacesinOrbit, je vois aussi votre point, mais le considère comme une manière différente de représenter le même contrôle. Mais, considérez que vous voudriez probablement passer new RetryPolicy().onRateLimitExceeded(STOP).onServiceTemporaryUnavailable(RETRY, 3)ou quelque chose, car il RateLimitExceededpourrait être nécessaire de gérer différemment ServiceTemporaryUnavailable. Après avoir écrit cela, ma pensée est: mieux vaut lever une exception, car cela donne un contrôle plus flexible.
Sjoerd Job Postmus
@SjoerdJobPostmus, je pense que cela dépend. Il se peut fort bien que la logique de limitation de débit et de nouvelle tentative fasse depuis dans votre bibliothèque, auquel cas je pense que mon approche est logique. S'il est plus logique de laisser cela à votre interlocuteur, jetez-le.
Winston Ewert