À la fin des années 90, j'ai beaucoup travaillé avec une base de code qui utilisait des exceptions comme contrôle de flux. Il a implémenté une machine à états finis pour piloter les applications de téléphonie. Dernièrement, je me souviens de ces jours parce que je faisais des applications Web MVC.
Ils ont tous les deux Controller
s qui décident où aller ensuite et fournissent les données à la logique de destination. Les actions des utilisateurs du domaine d'un téléphone à l'ancienne, comme les tonalités DTMF, sont devenues des paramètres pour les méthodes d'action, mais au lieu de renvoyer quelque chose comme un ViewResult
, ils ont lancé un StateTransitionException
.
Je pense que la principale différence était que les méthodes d'action étaient des fonctions void
. Je ne me souviens pas de tout ce que j'ai fait avec ce fait, mais j'hésitais même à me souvenir de beaucoup de choses parce que depuis ce travail, comme il y a 15 ans, je n'ai jamais vu cela dans le code de production à tout autre travail. J'ai supposé que c'était un signe qu'il s'agissait d'un soi-disant anti-modèle.
Est-ce le cas, et si oui, pourquoi?
Mise à jour: quand j'ai posé la question, j'avais déjà en tête la réponse de @ MasonWheeler, donc je suis allé avec la réponse qui a le plus ajouté à mes connaissances. Je pense que c'est aussi une bonne réponse.
Il y a une discussion détaillée à ce sujet sur Wiki de Ward . En général, l'utilisation d'exceptions pour le flux de contrôle est un anti-modèle, avec de nombreuses situations notables - et spécifiques au langage (voir pour exemplePython ) toux exceptions toux .
En résumé, pourquoi, généralement, c'est un anti-modèle:
Lisez la discussion sur le wiki de Ward pour des informations beaucoup plus approfondies.
Voir aussi un double de cette question, ici
Le cas d'utilisation pour lequel des exceptions ont été conçues est "Je viens de rencontrer une situation que je ne peux pas gérer correctement à ce stade, car je n'ai pas assez de contexte pour le gérer, mais la routine qui m'a appelé (ou quelque chose plus loin dans l'appel) pile) devrait savoir comment le gérer. "
Le cas d'utilisation secondaire est "Je viens de rencontrer une erreur grave, et pour l'instant, sortir de ce flux de contrôle pour éviter la corruption des données ou d'autres dommages est plus important que d'essayer de continuer."
Si vous n'utilisez pas d'exceptions pour l'une de ces deux raisons, il existe probablement une meilleure façon de le faire.
Les exceptions sont aussi puissantes que Continuations et GOTO
. Ils sont une construction de flux de contrôle universel.
Dans certains langages, ils sont la construction niquement du flux de contrôle universel. JavaScript, par exemple, n'a ni continuations ni GOTO
, il n'a même pas d'appels de queue appropriés. Donc, si vous voulez implémenter un flux de contrôle sophistiqué en JavaScript, vous avez pour utiliser les exceptions.
Le projet Microsoft Volta était un projet de recherche (désormais abandonné) visant à compiler du code .NET arbitraire en JavaScript. .NET a des exceptions dont la sémantique ne correspond pas exactement à JavaScript, mais plus important encore, il a Threads, et vous devez les mapper d'une manière ou d'une autre à JavaScript. Volta l'a fait en implémentant Volta Continuations à l'aide d'exceptions JavaScript, puis en implémentant toutes les constructions de flux de contrôle .NET en termes de Volta Continuations. Ils avaient pour utiliser les exceptions comme flux de contrôle, car il n'y a pas d'autre construction de flux de contrôle assez puissant.
Vous avez mentionné les State Machines. Les SM sont triviaux à implémenter avec des appels de queue appropriés: chaque état est un sous-programme, chaque transition d'état est un appel de sous-programme. Les SM peuvent également être facilement implémentés avec GOTO
ou Coroutines ou Continuations. Cependant, Java n'a aucun de ces quatre, mais il fait a des exceptions. Donc, il est parfaitement acceptable de les utiliser comme flux de contrôle. (Eh bien, en fait, le choix correct serait probablement d'utiliser un langage avec la construction de flux de contrôle appropriée, mais parfois vous pouvez être bloqué avec Java.)
Comme d'autres l'ont mentionné à plusieurs reprises, ( par exemple, dans cette question de débordement de pile ), le principe du moindre étonnement vous interdira d'utiliser excessivement les exceptions à des fins de contrôle de flux uniquement. D'un autre côté, aucune règle n'est correcte à 100%, et il y a toujours des cas où une exception est "juste le bon outil" - un peu comme goto
lui-même, soit dit en passant, qui se présente sous la forme de break
et continue
dans des langages comme Java, qui sont souvent le moyen idéal pour sortir des boucles fortement imbriquées, qui ne sont pas toujours évitables.
Le billet de blog suivant explique un cas d'utilisation assez complexe mais aussi plutôt intéressant pour un non local ControlFlowException
:
Il explique comment à l'intérieur de jOOQ (une bibliothèque d'abstraction SQL pour Java) (avertissement: je travaille pour le fournisseur), de telles exceptions sont parfois utilisées pour abandonner le processus de rendu SQL tôt quand une condition "rare" est rencontré.
Trop de valeurs de liaison sont rencontrées. Certaines bases de données ne prennent pas en charge des nombres arbitraires de valeurs de liaison dans leurs instructions SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). Dans ces cas, jOOQ abandonne la phase de rendu SQL et restitue l'instruction SQL avec des valeurs de liaison en ligne. Exemple:
// Pseudo-code attaching a "handler" that will
// abort query rendering once the maximum number
// of bind values was exceeded:
context.attachBindValueCounter();
String sql;
try {
// In most cases, this will succeed:
sql = query.render();
}
catch (ReRenderWithInlinedVariables e) {
sql = query.renderWithInlinedBindValues();
}
Si nous extrayions explicitement les valeurs de liaison de la requête AST pour les compter à chaque fois, nous gaspillerions de précieux cycles CPU pour les 99,9% des requêtes qui ne souffrent pas de ce problème.
Certaines logiques ne sont disponibles qu'indirectement via une API que nous ne voulons exécuter que "partiellement". La méthode UpdatableRecord.store()
génère une instruction INSERT
ou UPDATE
, selon les indicateurs internes de Record
. De "l'extérieur", nous ne savons pas quel type de logique est contenu dans store()
(par exemple verrouillage optimiste, gestion de l'écouteur d'événements, etc.) donc nous ne voulons pas répéter cette logique lorsque nous stockons plusieurs enregistrements dans une instruction batch, où nous aimerions que store()
ne génère que l'instruction SQL, pas réellement l'exécute. Exemple:
// Pseudo-code attaching a "handler" that will
// prevent query execution and throw exceptions
// instead:
context.attachQueryCollector();
// Collect the SQL for every store operation
for (int i = 0; i < records.length; i++) {
try {
records[i].store();
}
// The attached handler will result in this
// exception being thrown rather than actually
// storing records to the database
catch (QueryCollectorException e) {
// The exception is thrown after the rendered
// SQL statement is available
queries.add(e.query());
}
}
Si nous avions externalisé la logique store()
en API "réutilisable" qui peut être personnalisée pour éventuellement ne pas exécuter le SQL, nous envisagerais de créer une API assez difficile à maintenir et difficilement réutilisable.
Essentiellement, notre utilisation de ces goto
s non locaux va dans le sens de ce que Mason Wheeler a dit dans sa réponse:
"Je viens de rencontrer une situation que je ne peux pas gérer correctement à ce stade, car je n'ai pas assez de contexte pour le gérer, mais la routine qui m'a appelé (ou quelque chose plus haut dans la pile des appels) devrait savoir comment le gérer . "
Les deux utilisations de ControlFlowExceptions
étaient plutôt faciles à implémenter par rapport à leurs alternatives, ce qui nous permet de réutiliser un large éventail de logiques sans les refactoriser hors des internes pertinents.
Mais le sentiment que cela soit un peu une surprise pour les futurs responsables reste. Le code semble plutôt délicat et bien que ce soit le bon choix dans ce cas, nous préférons toujours ne pas utiliser d'exceptions pour le flux de contrôle local , où il est facile à éviter d'utiliser une ramification ordinaire via if - else
.
L'utilisation d'exceptions pour le flux de contrôle est généralement considérée comme un anti-modèle, mais il existe des exceptions (sans jeu de mots).
On a dit mille fois que les exceptions sont destinées à des conditions exceptionnelles. Une connexion à la base de données rompue est une condition exceptionnelle. Un utilisateur saisissant des lettres dans un champ de saisie qui ne doit autoriser que les chiffres n'est pas.
Un bogue dans votre logiciel qui provoque l'appel d'une fonction avec des arguments illégaux, par exemple null
là où cela n'est pas autorisé, is une condition exceptionnelle.
En utilisant des exceptions pour quelque chose qui n'est pas exceptionnel, vous utilisez des abstractions inappropriées pour le problème que vous essayez de résoudre.
Mais il peut également y avoir une pénalité de performance. Certaines langues ont une implémentation de gestion des exceptions plus ou moins efficace, donc si votre langue de choix n'a pas une gestion des exceptions efficace, cela peut être très coûteux, en termes de performances *.
Mais d'autres langages, par exemple Ruby, ont une syntaxe d'exception pour le flux de contrôle. Les situations exceptionnelles sont gérées par les opérateurs raise
/rescue
. Mais vous pouvez utiliser throw
/catch
pour les constructions de flux de contrôle de type exception **.
Ainsi, bien que les exceptions ne soient généralement pas utilisées pour le flux de contrôle, la langue de votre choix peut avoir d'autres idiomes.
* En exemple d'une utilisation coûteuse des exceptions pour les performances: j'ai déjà été configuré pour optimiser une application Web Form ASP.NET peu performante. Il s'est avéré que le rendu d'une grande table appelait int.Parse()
sur env. mille chaînes vides sur une page moyenne, ce qui donne environ. mille exceptions sont traitées. En remplaçant le code par int.TryParse()
j'ai rasé une seconde! Pour chaque demande de page unique!
** Cela peut être très déroutant pour un programmeur venant à Ruby à partir d'autres langues, car throw
et catch
sont des mots clés associés à des exceptions dans de nombreuses autres langues .
Il est tout à fait possible de gérer les conditions d'erreur sans utiliser d'exceptions. Certains langages, notamment C, n'ont même pas d'exceptions, et les gens parviennent toujours à créer des applications assez complexes avec lui. La raison pour laquelle les exceptions sont utiles est qu'elles vous permettent de spécifier succinctement deux flux de contrôle essentiellement indépendants dans le même code: un si une erreur se produit et un si elle ne se produit pas. Sans eux, vous vous retrouvez avec du code partout qui ressemble à ceci:
status = getValue(&inout);
if (status < 0)
{
logError("message");
return status;
}
doSomething(*inout);
Ou équivalent dans votre langue, comme retourner un Tuple avec une valeur comme statut d'erreur, etc. Souvent, les gens qui soulignent à quel point la gestion des exceptions est "coûteuse", négligent toutes les instructions if
supplémentaires comme ci-dessus qui vous sont nécessaires à ajouter si vous n'utilisez pas d'exceptions.
Bien que ce modèle se produise le plus souvent lors de la gestion d'erreurs ou d'autres "conditions exceptionnelles", à mon avis, si vous commencez à voir du code standard comme celui-ci dans d'autres circonstances, vous avez un assez bon argument pour utiliser des exceptions. Selon la situation et l'implémentation, je peux voir des exceptions utilisées valablement dans une machine à états, car vous avez deux flux de contrôle orthogonal: l'un qui change l'état et l'autre pour les événements qui se produisent dans les états.
Cependant, ces situations sont rares, et si vous voulez faire une exception (jeu de mots) à la règle, vous feriez mieux d'être prêt à montrer sa supériorité par rapport à d'autres solutions. Un écart sans une telle justification est appelé à juste titre un anti-modèle.
En Python, des exceptions sont utilisées pour la fin du générateur et de l'itération. Python a des blocs try/except très efficaces, mais en fait, lever une exception a une surcharge.
En raison du manque de ruptures à plusieurs niveaux ou d'une instruction goto en Python, j'ai parfois utilisé des exceptions:
class GOTO(Exception):
pass
try:
# Do lots of stuff
# in here with multiple exit points
# each exit point does a "raise GOTO()"
except GOTO:
pass
except Exception as e:
#display error
Esquissons une telle utilisation d'exception:
L'algorithme recherche récursivement jusqu'à ce que quelque chose soit trouvé. Donc, en revenant de la récursivité, il faut vérifier le résultat pour être trouvé, puis revenir, sinon continuer. Et cela revient à plusieurs reprises à partir d'une certaine profondeur de récursivité.
En plus d'avoir besoin d'un booléen supplémentaire found
(à emballer dans une classe, où sinon peut-être seulement un entier aurait été retourné), et pour la profondeur de récursivité le même postlude se produit.
Un tel déroulement d'une pile d'appels est exactement ce à quoi sert une exception. Il me semble donc qu'il s'agit d'un moyen de codage non-goto-like, plus immédiat et plus approprié. Pas nécessaire, utilisation rare, peut-être un mauvais style, mais au point. Comparable à l'opération de coupe Prolog.
Je pense que la façon la plus simple de répondre à cela est de comprendre les progrès OOP a fait au fil des ans. Tout a été fait en OOP (et la plupart des paradigmes de programmation, pour cela) matière) est modélisé autour nécessitant un travail effectué .
Chaque fois qu'une méthode est appelée, l'appelant dit "Je ne sais pas comment faire ce travail, mais vous savez comment, alors faites-le pour moi."
Cela présentait une difficulté: que se passe-t-il lorsque la méthode appelée sait généralement faire le travail, mais pas toujours? Nous avions besoin d'un moyen de communiquer "Je voulais vous aider, je l'ai vraiment fait, mais je ne peux pas faire ça."
Une première méthodologie pour communiquer cela consistait simplement à renvoyer une valeur "poubelle". Vous vous attendez peut-être à un entier positif, donc la méthode appelée renvoie un nombre négatif. Une autre façon d'y parvenir était de définir une valeur d'erreur quelque part. Malheureusement, les deux façons ont abouti à un code passe-partout laissez-moi-vérifier-ici-pour-vous assurer que tout est casher . À mesure que les choses se compliquent, ce système s'effondre.
Disons que vous avez un charpentier, un plombier et un électricien. Vous voulez plombier pour réparer votre évier, alors il y jette un œil. Ce n'est pas très utile s'il vous dit seulement, "Désolé, je ne peux pas le réparer. Il est cassé." Enfer, c'est encore pire s'il l'était pour jeter un coup d'œil, partir et vous envoyer une lettre disant qu'il ne pouvait pas le réparer. Maintenant, vous devez vérifier votre courrier avant même de savoir qu'il n'a pas fait ce que vous vouliez.
Ce que vous préféreriez, c'est qu'il vous le dise, "Ecoutez, je n'ai pas pu le réparer car il semble que votre pompe ne fonctionne pas."
Avec ces informations, vous pouvez conclure que vous voulez que l'électricien examine le problème. L'électricien trouvera peut-être quelque chose en rapport avec le charpentier, et vous devrez le faire réparer par le charpentier.
Heck, vous ne savez peut-être même pas que vous avez besoin d'un électricien, vous ne savez peut-être pas qui vous avez besoin. Vous n'êtes que des cadres intermédiaires dans une entreprise de réparation à domicile, et votre objectif est la plomberie. Vous dites donc que vous êtes le patron du problème, puis il dit à l'électricien de le résoudre.
C'est ce que les exceptions modélisent: des modes de défaillance complexes découplés. Le plombier n'a pas besoin de connaître l'électricien - il n'a même pas besoin de savoir que quelqu'un dans la chaîne peut résoudre le problème. Il rend simplement compte du problème qu'il a rencontré.
Ok, donc la compréhension du point des exceptions est la première étape. La prochaine consiste à comprendre ce qu'est un anti-modèle.
Pour être considéré comme un anti-modèle, il doit
Le premier point est facilement rencontré - le système a fonctionné, non?
Le deuxième point est plus collant. La principale raison de l'utilisation d'exceptions en tant que flux de contrôle normal est mauvaise, car ce n'est pas leur objectif. Tout élément de fonctionnalité donné dans un programme doit avoir un objectif relativement clair, et la cooptation de cet objectif conduit à une confusion inutile.
Mais ce n'est pas un mal définitif . C'est une mauvaise façon de faire les choses, et bizarre, mais un anti-modèle? Non, juste ... bizarre.