la-ferme

la-ferme

Exercice d'apprentissage

Introduction

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.

Lève une exception

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")

Gère les exceptions

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

Convention des méthodes

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.

Exceptions personnalisées

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

Instructions

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.

1. Répartis la nourriture équitablement

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

2. Vérifie le nombre de vaches

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.

  • Si le nombre de vaches transmis est supérieur à 0, la fonction doit appeler TheFarm.divide_food et renvoyer le résultat de cet appel.
  • Si le nombre de vaches est inférieur ou égal à 0, la fonction doit lever une erreur avec le message "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

3. Améliore la gestion des erreurs

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).

  • Si le nombre de vaches est inférieur à 0, la fonction renvoie une InvalidCowsError avec le message personnalisé défini sur "there are no negative cows".
  • Si le nombre de vaches est 0, la fonction renvoie une InvalidCowsError avec le message personnalisé défini sur "no cows don't need food".
  • Sinon, la fonction renvoie 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.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
Crystal Exercism

Prêt à commencer la-ferme ?

Inscris-toi sur Exercism pour apprendre et maîtriser Crystal avec 26 concepts142 exercices, et un vrai mentorat humain, le tout gratuitement.