Dans un monde idéal, tout fonctionne parfaitement. Mais dans le monde réel, les choses peuvent mal tourner, et la façon dont on gère ces situations est déterminante pour garantir que notre logiciel est robuste et fiable. Les exceptions sont un concept crucial en programmation : elles permettent de gérer les erreurs et les situations inattendues de manière élégante.
Si une erreur levée n'est pas gérée, le programme s'arrête et affiche un message d'erreur. Dans la plupart des cas, on ne veut pas que le programme s'arrête quand une erreur survient ; on préfère gérer cette erreur et continuer à exécuter le programme.
En Crystal, les exceptions se lèvent avec le mot-clé raise, et on peut lui donner soit une String, soit un objet Exception.
Si elle n'est pas gérée, le programme s'arrête et affiche le message d'erreur.
raise "This is an error"
Crystal possède plusieurs exceptions intégrées, comme ArgumentError, IndexError, KeyError, IOError, SystemCallError, TypeError, ZeroDivisionError et bien d'autres.
Celles-ci demandent de passer un message à l'exception.
raise ArgumentError.new("This is an argument error")
On ne veut pas que notre programme plante quand une exception est levée.
C'est pourquoi, quand on sait qu'un bout de code est susceptible de provoquer une erreur, on peut l'envelopper dans un bloc begin et rattraper l'exception avec un bloc rescue.
Le bloc begin marque le début du code susceptible de lever une exception, et le bloc rescue gère cette exception.
begin
raise "This is an error"
rescue
puts "An error occurred!"
end
On peut aussi associer une variable au bloc rescue pour récupérer l'objet exception.
begin
raise "This is an error"
rescue ex
puts "An error occurred: #{ex.message}"
end
On peut également préciser un type d'exception particulier dans le bloc rescue, pour ne rattraper que cette exception.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
end
# or
begin
raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
puts "An argument error occurred: #{ex.message}"
end
On peut utiliser plusieurs blocs rescue pour gérer différents types d'exceptions.
Dans l'exemple ci-dessous, le premier bloc rescue rattrape une ArgumentError, et le second rattrape toute autre exception.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
rescue
puts "An error occurred!"
end
Le bloc begin peut aussi avoir un bloc else, qui s'exécute si aucune exception n'est levée.
begin
puts "No error occurred"
rescue
puts "An error occurred!"
else
puts "No error occurred"
end
Enfin, il existe un bloc ensure qui s'exécute toujours, qu'une exception ait été levée ou non.
begin
raise "This is an error"
rescue
puts "An error occurred!"
ensure
puts "This is always executed"
end
Certaines méthodes existent en deux versions : une avec ! et une sans.
Cela peut vouloir dire deux choses différentes.
D'une part, la méthode modifie l'objet ; d'autre part, elle peut lever une exception.
Mais il existe aussi une autre convention, mentionnée dans le concept sur les booléens, qui consiste à terminer une méthode par ?.
Certaines méthodes lèvent une exception par défaut, mais possèdent aussi une version terminée par ? qui renvoie nil au lieu de lever une exception.
C'est idéal quand on veut éviter qu'une erreur soit levée. Cela peut améliorer les performances, puisqu'il n'y a pas de trace de pile à créer et que, si c'est bien fait, le code peut devenir plus sûr.
On peut aussi créer ses propres exceptions en héritant de la classe Exception.
Pour ce faire, on peut redéfinir la méthode initialize afin de définir le message de l'exception.
Il suffit pour cela d'affecter le message à une variable d'instance nommée @message.
class MyException < Exception
def initialize
@message = "This is my exception"
end
end
raise MyException.new
Le jour que tu attendais depuis si longtemps est enfin arrivé : tu es désormais le fier propriétaire d'une belle ferme dans les Alpes.
Tu n'aimes toujours pas te lever trop tôt le matin pour nourrir tes vaches.
Comme tu es un excellent ingénieur, tu construis un distributeur de nourriture, le FEED-M-ALL.
La dernière chose dont tu as besoin pour terminer ton projet, c'est un bout de code qui calcule la quantité de fourrage que chaque vache doit recevoir. Il est important que chaque vache reçoive la même quantité ; il faut éviter les conflits. Les vaches sont très sensibles.
Heureusement, tu n'as pas besoin de trouver toi-même toutes les formules pour calculer les quantités de fourrage. Tu utilises une mystérieuse bibliothèque externe que tu as trouvée sur internet. Elle est censée produire les vaches les plus heureuses. La bibliothèque expose une classe qui implémente l'interface suivante. Tu t'appuieras sur elle dans le code que tu écriras toi-même.
class FodderCalculator
def fodder_amount! : Number
end
def fattening_factor! : Number
end
end
Au fur et à mesure que tu travailles sur ton code, tu amélioreras la gestion des erreurs pour la rendre plus robuste et plus facile à déboguer plus tard, quand tu l'utiliseras dans ta vie quotidienne à la ferme.
Tout d'abord, tu te concentres sur l'écriture du code nécessaire pour calculer la quantité de fourrage par vache.
Implémente une méthode TheFarm.divide_food qui accepte comme arguments un objet FodderCalculator et un nombre de vaches sous forme d'entier.
Pour cette tâche, tu supposes que le nombre de vaches transmis est toujours supérieur à zéro.
La méthode doit renvoyer la quantité de nourriture par vache.
Pour faire le calcul, tu dois d'abord récupérer la quantité totale de fourrage pour toutes les vaches.
Pour cela, il faut appeler la méthode fodder_amount! en lui passant le nombre de vaches.
De plus, tu as besoin d'un facteur par lequel cette quantité doit être multipliée.
Tu obtiens ce facteur en appelant la méthode fattening_factor!.
Avec ces deux valeurs et le nombre de vaches, tu peux maintenant calculer la quantité de nourriture par vache (sous forme de float64).
Si l'une des méthodes que tu appelles renvoie une erreur, l'exécution doit s'arrêter et renvoyer 0.
fodder_calculator = FodderCalculator.new(50, 1.5)
TheFarm.divide_food(fodder_calculator, 5)
# => 15.0
fodder_calculator = FodderCalculator.new(50, nil)
TheFarm.divide_food(fodder_calculator, 5)
# => 0
En travaillant sur la première tâche ci-dessus, tu as réalisé que la bibliothèque externe que tu utilises n'est pas d'aussi bonne qualité que tu le pensais. Par exemple, elle ne sait pas gérer correctement les entrées invalides. Tu veux contourner cette limitation en ajoutant une vérification de la valeur d'entrée dans ton propre code.
Écris une fonction TheFarm.validate_input_and_divide_food qui a la même signature que TheFarm.divide_food ci-dessus.
TheFarm.divide_food et renvoyer le résultat de cet appel."Number of cows must be greater than 0".TheFarm.validate_input_and_divide_food(fodderCalculator, 5)
# => 15
TheFarm.validate_input_and_divide_food(fodderCalculator, -2)
# Error: Number of cows must be greater than 0
Vérifier le nombre de vaches avant de le transmettre était une bonne idée, mais le message d'erreur trop vague ne te satisfait pas vraiment.
Tu décides de faire mieux en créant une classe d'erreur personnalisée appelée InvalidCowsError.
L'erreur personnalisée doit contenir le nombre de vaches (Int32) et un message personnalisé (String), et la méthode Expection doit sérialiser les données au format suivant :
{number of cows} cows are invalid: {custom message}
Muni de ton erreur personnalisée, implémente une fonction TheFarm.validate_number_of_cows qui accepte le nombre de vaches sous forme d'entier et renvoie une erreur (ou nil).
InvalidCowsError avec le message personnalisé défini sur "there are no negative cows".InvalidCowsError avec le message personnalisé défini sur "no cows don't need food".nil pour indiquer que la validation a réussi.TheFarm.validate_number_of_cows(5)
# => nil
TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows
Après le dur labeur que représente la mise en place de cette fonction de validation, tu remarques qu'il se fait déjà tard et tu quittes ton bureau pour profiter du coucher de soleil sur les montagnes. Tu remets à un autre jour le soin d'ajouter réellement la nouvelle fonction de validation à ton code.
Inscris-toi sur Exercism pour apprendre et maîtriser Crystal avec 26 concepts142 exercices, et un vrai mentorat humain, le tout gratuitement.