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.
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")
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
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.
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
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.
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
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.
TheFarm.divide_food y devolver el resultado de esa llamada."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
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).
InvalidCowsError con el mensaje personalizado establecido en "there are no negative cows".InvalidCowsError con el mensaje personalizado establecido en "no cows don't need food".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.
Regístrate en Exercism para aprender y dominar Crystal con 26 conceptos142 ejercicios y mentoría humana real, todo gratis.