La granja

La granja

Ejercicio de aprendizaje

Introducción

En un mundo ideal, todo funciona a la perfección. Pero en el mundo real las cosas pueden salir mal, y cómo gestionamos estas situaciones es importante para garantizar que nuestro software sea robusto y fiable. Las excepciones son un concepto fundamental en programación que nos permite gestionar errores y situaciones inesperadas con elegancia.

Lanzar un error, si no se gestiona, detiene el programa y muestra un mensaje de error. En la mayoría de los casos, no quieres que tu programa se detenga cuando se produce un error; en su lugar, quieres gestionar el error y que el programa siga ejecutándose.

Lanzar una excepción

En Crystal, las excepciones se lanzan con la palabra clave raise y se les puede dar un String o un objeto Exception. Si no se gestionan, el programa se detendrá e imprimirá el mensaje de error.

raise "This is an error"

Hay varias excepciones integradas en Crystal, como ArgumentError, IndexError, KeyError, IOError, SystemCallError, TypeError, ZeroDivisionError y muchas más. Estas requieren que le pases un mensaje a la excepción.

raise ArgumentError.new("This is an argument error")

Gestionar excepciones

No querríamos que nuestro programa se bloquease cuando se lanza una excepción. Por lo tanto, cuando sabemos que un fragmento de código es propenso a errores, podemos envolverlo en un bloque begin y capturar la excepción con un bloque rescue. El bloque begin marca el comienzo del código que podría lanzar una excepción, y el bloque rescue se encarga de gestionarla.

begin
 raise "This is an error"
rescue
 puts "An error occurred!"
end

Al bloque rescue también se le puede indicar una variable para obtener el objeto de la excepción.

begin
 raise "This is an error"
rescue ex
 puts "An error occurred: #{ex.message}"
end

Al bloque rescue también se le puede indicar un tipo de excepción concreto para capturar únicamente esa excepción.

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

Se pueden usar varios bloques rescue para gestionar distintos tipos de excepciones. En el ejemplo siguiente, el primer bloque rescue capturará un ArgumentError y el segundo bloque rescue capturará cualquier otra excepción.

begin
 raise ArgumentError.new("This is an argument error")
rescue ArgumentError
 puts "An argument error occurred!"
rescue
 puts "An error occurred!"
end

El bloque begin también puede tener un bloque else, que se ejecuta si no se lanza ninguna excepción.

begin
 puts "No error occurred"
rescue
 puts "An error occurred!"
else
 puts "No error occurred"
end

Por último, existe un bloque ensure que siempre se ejecuta, independientemente de si se lanzó una excepción.

begin
 raise "This is an error"
rescue
 puts "An error occurred!"
ensure
 puts "This is always executed"
end

Convención de los métodos

Algunos métodos tienen dos versiones: una con ! y otra sin él. Esto puede significar dos cosas distintas. Una es que el método modifica el objeto, y la otra es que el método puede lanzar una excepción.

Pero también existe otra convención sobre terminar un método con ?, mencionada en el concepto de booleanos. Algunos métodos lanzan una excepción de forma predeterminada, pero también tienen una versión que termina con ? y que devuelve nil en lugar de lanzar una excepción.

Esto es ideal cuando quieres evitar que se lance un error. Puede beneficiar al rendimiento, ya que no tiene que crear una traza de pila y, si se configura correctamente, puede hacer que el código sea más seguro.

Excepciones personalizadas

También puedes crear tus propias excepciones heredando de la clase Exception. Al hacerlo, puedes sobrescribir opcionalmente el método initialize para establecer el mensaje de la excepción. Esto se puede hacer asignando el mensaje a una variable de instancia llamada @message.

class MyException < Exception
 def initialize
   @message = "This is my exception"
 end
end

raise MyException.new

Instrucciones

Por fin ha llegado el día que tanto esperabas y ahora eres el orgulloso propietario de una preciosa granja en los Alpes.

Sigues sin querer madrugar demasiado para alimentar a tus vacas. Como eres un ingeniero excelente, construyes un dispensador de comida, el FEED-M-ALL.

Lo último que te hace falta para terminar tu proyecto es un fragmento de código que calcule la cantidad de forraje que debe recibir cada vaca. Es importante que cada vaca reciba la misma cantidad; hay que evitar conflictos. Las vacas son muy sensibles.

Por suerte, no tienes que idear tú mismo todas las fórmulas para calcular las cantidades de forraje. Usas una misteriosa biblioteca externa que encontraste en internet. Se supone que da como resultado las vacas más felices. La biblioteca expone una clase que cumple la siguiente interfaz. Te basarás en ella en el código que escribas tú mismo.

class FodderCalculator
  def fodder_amount! : Number 
  end

  def fattening_factor! : Number
  end
end

A medida que avances con tu código, irás mejorando el tratamiento de errores para que sea más robusto y más fácil de depurar cuando lo uses en tu día a día en la granja.

1. Reparte la comida a partes iguales

En primer lugar, te centras en escribir el código necesario para calcular la cantidad de forraje por vaca.

Implementa un método TheFarm.divide_food que acepte como argumentos un objeto FodderCalculator y un número de vacas como entero. Para esta tarea, asumes que el número de vacas que se pasa siempre es mayor que cero. El método debe devolver la cantidad de comida por vaca.

Para hacer el cálculo, primero necesitas obtener la cantidad total de forraje para todas las vacas. Esto se hace llamando al método fodder_amount! y pasándole el número de vacas. Además, necesitas un factor por el que hay que multiplicar esa cantidad. Obtienes ese factor llamando al método fattening_factor!. Con estos dos valores y el número de vacas, ya puedes calcular la cantidad de comida por vaca (como un float64).

Si uno de los métodos que llamas devuelve un error, la ejecución debe detenerse y devolver 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. Comprueba el número de vacas

Mientras trabajabas en la primera tarea de arriba, te diste cuenta de que la biblioteca externa que usas no tiene tanta calidad como pensabas. Por ejemplo, no sabe gestionar correctamente las entradas no válidas. Quieres sortear esa limitación añadiendo una comprobación del valor de entrada en tu propio código.

Escribe una función TheFarm.validate_input_and_divide_food que tenga la misma signatura que TheFarm.divide_food de arriba.

  • Si el número de vacas que se pasa es mayor que 0, la función debe llamar a TheFarm.divide_food y devolver el resultado de esa llamada.
  • Si el número de vacas es 0 o menos, la función debe lanzar un error con el mensaje "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. Mejora el tratamiento de errores

Comprobar el número de vacas antes de pasarlo fue un buen movimiento, pero no estás del todo contento con el mensaje de error tan poco específico. Decides hacerlo mejor creando una clase de error personalizada llamada InvalidCowsError.

El error personalizado debe contener el número de vacas (Int32) y un mensaje personalizado (String), y el método Expection debe serializar los datos con el siguiente formato:

{number of cows} cows are invalid: {custom message}

Ya con tu error personalizado, implementa una función TheFarm.validate_number_of_cows que acepte el número de vacas como entero y devuelva un error (o nil).

  • Si el número de vacas es menor que 0, la función devuelve un InvalidCowsError con el mensaje personalizado establecido en "there are no negative cows".
  • Si el número de vacas es 0, la función devuelve un InvalidCowsError con el mensaje personalizado establecido en "no cows don't need food".
  • En caso contrario, la función devuelve nil para indicar que la validación se ha realizado correctamente.
TheFarm.validate_number_of_cows(5)
# => nil

TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows

Después del duro trabajo de preparar esta función de validación, te das cuenta de que ya ha caído la tarde y dejas tu escritorio para disfrutar del atardecer sobre las montañas. Dejas para otro día la tarea de añadir de verdad la nueva función de validación a tu código.

Editar en GitHub El enlace se abre en una ventana o pestaña nueva
Crystal Exercism

¿Listo para empezar La granja?

Regístrate en Exercism para aprender y dominar Crystal con 26 conceptos142 ejercicios y mentoría humana real, todo gratis.