Les exceptions sont aussi anciennes que la programmation elle-même. À l'époque où la programmation était effectuée en matériel ou via des langages de programmation de bas niveau, des exceptions étaient utilisées pour modifier le flux du programme et pour éviter les pannes matérielles. Aujourd'hui, Wikipedia définit les exceptions comme:
conditions anormales ou exceptionnelles nécessitant un traitement spécial - modifiant souvent le déroulement normal de l'exécution du programme…
Et que leur manipulation nécessite:
constructions de langage de programmation spécialisé ou mécanismes de matériel informatique.
Ainsi, les exceptions nécessitent un traitement spécial et une exception non gérée peut provoquer un comportement inattendu. Les résultats sont souvent spectaculaires. En 1996, le célèbre Échec du lancement de la fusée Ariane 5 a été attribuée à une exception de dépassement de capacité non gérée. Les pires bogues logiciels de l’histoire contient quelques autres bogues qui pourraient être attribués à des exceptions non gérées ou mal gérées.
Au fil du temps, ces erreurs et d'innombrables autres (qui n'étaient peut-être pas aussi dramatiques, mais toujours catastrophiques pour les personnes impliquées) ont contribué à l'impression que les exceptions sont mauvaises .
Mais les exceptions sont un élément fondamental de la programmation moderne; ils existent pour améliorer nos logiciels. Plutôt que de craindre les exceptions, nous devrions les adopter et apprendre à en tirer profit. Dans cet article, nous verrons comment gérer les exceptions avec élégance et les utiliser pour écrire du code propre qui est plus facile à gérer.
Avec la montée de programmation orientée objet (POO), la prise en charge des exceptions est devenue un élément crucial des langages de programmation modernes. Un système robuste de gestion des exceptions est intégré dans la plupart des langues, de nos jours. Par exemple, Ruby fournit le modèle typique suivant:
begin do_something_that_might_not_work! rescue SpecificError => e do_some_specific_error_clean_up retry if some_condition_met? ensure this_will_always_be_executed end
Il n'y a rien de mal avec le code précédent. Mais une utilisation excessive de ces modèles entraînera des odeurs de code et ne sera pas nécessairement bénéfique. De même, leur mauvaise utilisation peut en fait nuire considérablement à votre base de code, la rendre fragile ou masquer la cause des erreurs.
La stigmatisation entourant les exceptions fait souvent que les programmeurs se sentent perdus. C’est une réalité de la vie que les exceptions ne peuvent être évitées, mais on nous apprend souvent qu’elles doivent être traitées rapidement et de manière décisive. Comme nous le verrons, ce n'est pas nécessairement vrai. Nous devrions plutôt apprendre l'art de gérer les exceptions avec élégance, en les rendant harmonieuses avec le reste de notre code.
Voici quelques pratiques recommandées qui vous aideront à accepter les exceptions et à les utiliser ainsi que leurs capacités à conserver votre code maintenable , extensible , et lisible :
Ces éléments sont les principaux facteurs de ce que nous pourrions appeler propreté ou qualité , qui n'est pas une mesure directe en soi, mais plutôt l'effet combiné des points précédents, comme démontré dans cette bande dessinée:
Cela dit, plongeons dans ces pratiques et voyons comment chacune d’elles affecte ces trois mesures.
Remarque: Nous présenterons des exemples de Ruby, mais toutes les constructions présentées ici ont des équivalents dans les langages POO les plus courants.
ApplicationError
hiérarchieLa plupart des langages sont livrés avec une variété de classes d'exceptions, organisées dans une hiérarchie d'héritage, comme toute autre classe OOP. Pour préserver la lisibilité, la maintenabilité et l'extensibilité de notre code, il est judicieux de créer notre propre sous-arborescence d'exceptions spécifiques à l'application qui étendent la classe d'exceptions de base. Investir du temps dans la structuration logique de cette hiérarchie peut être extrêmement bénéfique. Par exemple:
class ApplicationError 
requêtes média pour un design réactif
Le fait de disposer d'un package d'exceptions extensible et complet pour notre application facilite grandement la gestion de ces situations spécifiques à l'application. Par exemple, nous pouvons décider des exceptions à gérer de manière plus naturelle. Cela augmente non seulement la lisibilité de notre code, mais augmente également la maintenabilité de nos applications et bibliothèques (gemmes).
Du point de vue de la lisibilité, c'est beaucoup plus facile à lire:
rescue ValidationError => e
Que de lire:
rescue RequiredFieldError, UniqueFieldError, ... => e
Du point de vue de la maintenabilité, disons, par exemple, nous implémentons une API JSON, et nous avons défini la nôtre ClientError
avec plusieurs sous-types, à utiliser lorsqu'un client envoie une mauvaise demande. Si l'un de ces éléments est déclenché, l'application doit afficher la représentation JSON de l'erreur dans sa réponse. Il sera plus facile de corriger ou d'ajouter de la logique à un seul bloc qui gère les ClientError
s plutôt que de boucler sur chaque erreur client possible et d'implémenter le même code de gestionnaire pour chacun. En termes d'extensibilité, si nous devons implémenter plus tard un autre type d'erreur client, nous pouvons être sûrs qu'elle sera déjà gérée correctement ici.
De plus, cela ne nous empêche pas d'implémenter une gestion spéciale supplémentaire pour des erreurs client spécifiques plus tôt dans la pile d'appels, ou de modifier le même objet d'exception en cours de route:
# app/controller/pseudo_controller.rb def authenticate_user! fail AuthenticationError if token_invalid? || token_expired? User.find_by(authentication_token: token) rescue AuthenticationError => e report_suspicious_activity if token_invalid? raise e end def show authenticate_user! show_private_stuff!(params[:id]) rescue ClientError => e render_error(e) end
Comme vous pouvez le voir, la levée de cette exception spécifique ne nous a pas empêchés de pouvoir la gérer à différents niveaux, de la modifier, de la relancer et de permettre au gestionnaire de classe parent de la résoudre.
Deux choses à noter ici:
- Tous les langages ne prennent pas en charge la levée d'exceptions à partir d'un gestionnaire d'exceptions.
- Dans la plupart des langues, élever un Nouveau une exception depuis un gestionnaire entraînera la perte définitive de l'exception d'origine, il est donc préférable de relancer le même objet d'exception (comme dans l'exemple ci-dessus) pour éviter de perdre la trace de la cause d'origine de l'erreur. (Sauf si vous faites cela intentionnellement ).
Jamais rescue Exception
Autrement dit, n'essayez jamais d'implémenter un gestionnaire fourre-tout pour le type d'exception de base. Sauver ou attraper toutes les exceptions en gros est jamais une bonne idée dans n'importe quel langage, que ce soit globalement au niveau de l'application de base, ou dans une petite méthode enterrée utilisée une seule fois. Nous ne voulons pas sauver Exception
car cela obscurcira ce qui s'est réellement passé, endommageant à la fois la maintenabilité et l'extensibilité. Nous pouvons perdre énormément de temps à déboguer le problème réel, alors que cela pourrait être aussi simple qu'une erreur de syntaxe:
# main.rb def bad_example i_might_raise_exception! rescue Exception nah_i_will_always_be_here_for_you end # elsewhere.rb def i_might_raise_exception! retrun do_a_lot_of_work! end
Vous avez peut-être remarqué l'erreur dans l'exemple précédent; return
est mal saisi. Bien que les éditeurs modernes offrent une certaine protection contre ce type spécifique d'erreur de syntaxe, cet exemple illustre comment rescue Exception
nuit à notre code. À aucun moment le type réel de l'exception (dans ce cas, un NoMethodError
) n'est adressé, ni jamais exposé au développeur, ce qui peut nous faire perdre beaucoup de temps à tourner en rond.
Jamais rescue
plus d'exceptions que nécessaire
Le point précédent est un cas particulier de cette règle: nous devons toujours faire attention à ne pas trop généraliser nos gestionnaires d'exceptions. Les raisons sont les mêmes; chaque fois que nous sauvons plus d'exceptions que nous ne le devrions, nous finissons par cacher des parties de la logique de l'application aux niveaux supérieurs de l'application, sans parler de la suppression de la capacité du développeur à gérer l'exception lui-même. Cela affecte gravement l'extensibilité et la maintenabilité du code.
Si nous essayons de gérer différents sous-types d'exceptions dans le même gestionnaire, nous introduisons des blocs de code lourd qui ont trop de responsabilités. Par exemple, si nous construisons une bibliothèque qui consomme une API distante, gérer un MethodNotAllowedError
(HTTP 405), est généralement différent de la gestion d'un UnauthorizedError
(HTTP 401), même s'ils sont tous les deux ResponseError
s.
Comme nous le verrons, il existe souvent une partie différente de l'application qui serait mieux adaptée pour gérer des exceptions spécifiques dans un plus SEC façon.
Alors, définissez le responsabilité unique de votre classe ou méthode, et gérer le strict minimum d'exceptions qui satisfont à cette exigence de responsabilité . Par exemple, si une méthode est responsable de obtenir des informations sur les stocks à partir d'une API distante, il devrait alors gérer les exceptions qui découlent de l'obtention de ces informations uniquement, et laisser le traitement des autres erreurs à une méthode différente conçue spécifiquement pour ces responsabilités:
def get_info begin response = HTTP.get(STOCKS_URL + '#{@symbol}/info') fail AuthenticationError if response.code == 401 fail StockNotFoundError, @symbol if response.code == 404 return JSON.parse response.body rescue JSON::ParserError retry end end
Ici, nous avons défini le contrat pour cette méthode pour nous fournir uniquement les informations sur le stock. Il gère erreurs spécifiques au point final , comme une réponse JSON incomplète ou mal formée. Il ne gère pas le cas où l'authentification échoue ou expire, ou si le stock n'existe pas. Celles-ci sont la responsabilité de quelqu'un d'autre et sont explicitement transmises à la pile d'appels où il devrait y avoir un meilleur endroit pour gérer ces erreurs de manière DRY.
Résistez à l'envie de gérer les exceptions immédiatement
Ceci est le complément du dernier point. Une exception peut être gérée à n'importe quel point de la pile d'appels, et à n'importe quel point de la hiérarchie des classes, donc savoir exactement où la gérer peut être mystérieux. Pour résoudre cette énigme, de nombreux développeurs choisissent de gérer toute exception dès qu'elle survient, mais investir du temps pour y réfléchir aboutira généralement à trouver un endroit plus approprié pour gérer des exceptions spécifiques.
Un modèle courant que nous voyons dans les applications Rails ( en particulier ceux qui exposent des API uniquement JSON ) est la méthode de contrôle suivante:
# app/controllers/client_controller.rb def create @client = Client.new(params[:client]) if @client.save render json: @client else render json: @client.errors end end
(Notez que bien que ce ne soit pas techniquement un gestionnaire d'exceptions, fonctionnellement, il sert le même objectif, car @client.save
ne renvoie false que lorsqu'il rencontre une exception.)
Dans ce cas, cependant, répéter le même gestionnaire d'erreurs dans chaque action du contrôleur est l'opposé de DRY et endommage la maintenabilité et l'extensibilité. Au lieu de cela, nous pouvons utiliser la nature particulière de la propagation d'exceptions et les gérer une seule fois, dans la classe de contrôleur parent , ApplicationController
:
# app/controllers/client_controller.rb def create @client = Client.create!(params[:client]) render json: @client end
# app/controller/application_controller.rb rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity def render_unprocessable_entity(e) render json: { errors: e.record.errors }, status: 422 end
De cette façon, nous pouvons nous assurer que tous les ActiveRecord::RecordInvalid
les erreurs sont correctement et DRY-ly gérées en un seul endroit, sur la base ApplicationController
niveau. Cela nous donne la liberté de les manipuler si nous voulons traiter des cas spécifiques au niveau inférieur, ou simplement les laisser se propager gracieusement.
Toutes les exceptions ne nécessitent pas de traitement
Lors du développement d'une gemme ou d'une bibliothèque, de nombreux développeurs essaieront d'encapsuler la fonctionnalité et n'autoriseront aucune exception à se propager hors de la bibliothèque. Mais parfois, il n’est pas évident de gérer une exception tant que l’application spécifique n’est pas mise en œuvre.
Prenons ActiveRecord
comme exemple de la solution idéale. La bibliothèque fournit aux développeurs deux approches pour l'exhaustivité. La save
La méthode gère les exceptions sans les propager, en retournant simplement false
, tandis que save!
lève une exception en cas d'échec. Cela donne aux développeurs la possibilité de gérer différemment des cas d'erreur spécifiques, ou simplement de gérer tout échec de manière générale.
Mais que se passe-t-il si vous n’avez ni le temps ni les ressources pour fournir une mise en œuvre aussi complète? Dans ce cas, s'il y a une incertitude, il vaut mieux exposer l'exception et relâchez-le dans la nature.
Voici pourquoi: nous travaillons avec des exigences en mouvement presque tout le temps, et prendre la décision qu'une exception sera toujours gérée d'une manière spécifique pourrait en fait nuire à notre implémentation, endommager l'extensibilité et la maintenabilité, et potentiellement ajouter d'énormes dette technique , en particulier lors du développement de bibliothèques.
Prenons l'exemple précédent d'un consommateur d'API stock qui récupère les cours des actions. Nous avons choisi de gérer la réponse incomplète et mal formée sur place, et nous avons choisi de réessayer la même demande jusqu'à ce que nous obtenions une réponse valide. Mais plus tard, les exigences peuvent changer, de sorte que nous devons revenir aux données de stock historiques enregistrées, au lieu de réessayer la demande.
À ce stade, nous serons obligés de modifier la bibliothèque elle-même, en mettant à jour la manière dont cette exception est gérée, car les projets dépendants ne gèrent pas cette exception. (Comment pourraient-ils? Cela ne leur a jamais été exposé auparavant.) Nous devrons également informer les propriétaires de projets qui dépendent de notre bibliothèque. Cela pourrait devenir un cauchemar s'il existe de nombreux projets de ce type, car ils ont probablement été construits sur l'hypothèse que cette erreur sera gérée de manière spécifique.
Maintenant, nous pouvons voir où nous allons avec la gestion des dépendances. Les perspectives ne sont pas bonnes. Cette situation se produit assez souvent, et le plus souvent, elle dégrade l’utilité, l’extensibilité et la flexibilité de la bibliothèque.
Alors, voici l'essentiel: s'il n'est pas clair comment une exception doit être gérée, laissez-la se propager gracieusement . Il existe de nombreux cas où une place claire existe pour gérer l'exception en interne, mais il existe de nombreux autres cas où il est préférable d'exposer l'exception. Donc, avant d'opter pour la gestion de l'exception, réfléchissez-y une seconde. Une bonne règle de base est de insister sur la gestion des exceptions lorsque vous interagissez directement avec l'utilisateur final.
Suivez la convention
L'implémentation de Ruby, et plus encore de Rails, suit certaines conventions de dénomination, telles que la distinction entre method_names
et method_names!
avec un «bang». Dans Ruby, le bang indique que la méthode modifiera l'objet qui l'a invoquée, et dans Rails, cela signifie que la méthode lèvera une exception si elle ne parvient pas à exécuter le comportement attendu. Essayez de respecter la même convention, surtout si vous allez ouvrir votre bibliothèque en open source.
Si nous devions écrire un nouveau method!
avec un bang dans une application Rails, il faut tenir compte de ces conventions. Rien ne nous oblige à lever une exception lorsque cette méthode échoue, mais en s'écartant de la convention, cette méthode peut induire en erreur les programmeurs en leur faisant croire qu'ils auront la possibilité de gérer eux-mêmes les exceptions, alors qu'en fait, ils ne le feront pas.
Une autre convention Ruby, attribuée à Jim Weirich, est de utiliser fail
pour indiquer l'échec de la méthode , et uniquement à utiliser raise
si vous relancez l'exception.
Un aparté, parce que j'utilise des exceptions pour indiquer les échecs, j'utilise presque toujours le fail
mot clé plutôt que le raise
mot-clé en Ruby. Échec et augmentation sont des synonymes, il n'y a donc pas de différence sauf que l'échec indique plus clairement que la méthode a échoué. Le seul moment où j'utilise lever, c'est lorsque j'attrape une exception et que je la relance, car ici je n'échoue pas, mais je lève explicitement et délibérément une exception. C'est un problème stylistique que je suis, mais je doute que beaucoup d'autres le fassent.
De nombreuses autres communautés linguistiques ont adopté des conventions comme celles-ci sur la manière dont les exceptions sont traitées, et ignorer ces conventions nuira à la lisibilité et à la maintenabilité de notre code.
Logger.log (tout)
Cette pratique ne s’applique pas uniquement aux exceptions, bien sûr, mais s’il y a une chose qui devrait toujours être connecté, c'est une exception.
La journalisation est extrêmement importante (suffisamment importante pour que Ruby expédie un journaux avec sa version standard). C’est le journal de nos applications, et encore plus important que de garder une trace de la façon dont nos applications réussissent, c’est consigner comment et quand elles échouent.
Il ne manque pas de bibliothèques de journalisation ou services basés sur les journaux et modèles de conception. Il est essentiel de garder une trace de nos exceptions afin que nous puissions examiner ce qui s'est passé et enquêter si quelque chose ne va pas. Des messages de journal appropriés peuvent diriger les développeurs directement vers la cause d'un problème, leur faisant gagner un temps incommensurable.
Cette confiance propre au code
Une gestion propre des exceptions enverra la qualité de votre code à la lune! Tweet Les exceptions sont une partie fondamentale de chaque langage de programmation. Ils sont spéciaux et extrêmement puissants, et nous devons tirer parti de leur pouvoir pour élever la qualité de notre code au lieu de nous épuiser à nous battre avec eux.
Dans cet article, nous avons plongé dans quelques bonnes pratiques pour structurer nos arbres d'exceptions et comment il peut être bénéfique pour la lisibilité et la qualité de les structurer logiquement. Nous avons examiné différentes approches pour gérer les exceptions, soit en un seul endroit, soit à plusieurs niveaux.
Nous avons vu que c’était mal de les «attraper tous» et que ce n’était pas grave de les laisser flotter et faire des bulles.
convertir le serveur sql en oracle
Nous avons examiné où gérer les exceptions de manière SÈCHE et avons appris que nous ne sommes pas obligés de les gérer quand et où elles surviennent pour la première fois.
Nous avons discuté quand exactement est c'est une bonne idée de les gérer, quand c'est une mauvaise idée, et pourquoi, en cas de doute, c'est une bonne idée de les laisser se propager.
Enfin, nous avons discuté d'autres points qui peuvent aider à maximiser l'utilité des exceptions, comme suivre les conventions et tout consigner.
Avec ces directives de base, nous pouvons nous sentir beaucoup plus à l'aise et en confiance pour gérer les cas d'erreur dans notre code et rendre nos exceptions vraiment exceptionnelles!
Remerciement spécial à Avdi Grimm et son discours génial Rubis exceptionnel , qui a beaucoup aidé à la réalisation de cet article.
En relation: Conseils et bonnes pratiques pour les développeurs Ruby