a-quinta

a-quinta

Exercício de aprendizagem

Introdução

No mundo ideal, tudo funciona perfeitamente. Mas, no mundo real, as coisas podem correr mal, e a forma como tratamos estas situações é importante para garantir que o nosso software é robusto e fiável. As exceções são um conceito fundamental na programação que nos permite tratar erros e situações inesperadas com elegância.

Lançar um erro, se não for tratado, interrompe o programa e lança uma mensagem de erro. Na maioria dos casos, não queres que o programa pare quando ocorre um erro; queres antes tratar o erro e continuar a executar o programa.

Lançar uma exceção

No Crystal, as exceções são lançadas com a palavra-chave raise e podem receber uma String ou um objeto Exception. Se não for tratada, o programa para e imprime a mensagem de erro.

raise "This is an error"

Existem várias exceções incorporadas no Crystal, como ArgumentError, IndexError, KeyError, IOError, SystemCallError, TypeError, ZeroDivisionError e muitas mais. Estas exigem que passes uma mensagem à exceção.

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

Tratar exceções

Não queremos que o nosso programa vá abaixo quando é lançada uma exceção. Por isso, quando sabemos que um trecho de código é propício a erros, podemos envolvê-lo num bloco begin e capturar a exceção com um bloco rescue. O bloco begin marca o início do código que pode lançar uma exceção, e o bloco rescue trata a exceção.

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

O bloco rescue também pode ser especificado com uma variável para obter o objeto da exceção.

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

O bloco rescue também pode indicar um tipo específico de exceção, para capturar apenas essa exceção.

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

Podem usar-se vários blocos rescue para tratar diferentes tipos de exceções. No exemplo abaixo, o primeiro bloco rescue captura um ArgumentError, e o segundo bloco rescue captura qualquer outra exceção.

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

O bloco begin também pode ter um bloco else, que é executado se não for lançada nenhuma exceção.

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

Por último, existe um bloco ensure que é sempre executado, independentemente de ter sido lançada uma exceção.

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

Convenção de métodos

Alguns métodos têm duas versões: uma com ! e outra sem. Isto pode significar duas coisas diferentes. Uma é que o método altera o objeto; a outra é que o método pode lançar uma exceção.

Mas existe também outra convenção, mencionada no conceito de Boolean, de terminar um método com ?. Alguns métodos lançam uma exceção por predefinição, mas têm também uma versão terminada em ? que devolve nil em vez de lançar uma exceção.

Isto é ideal quando queres evitar que seja lançado um erro. Isto pode beneficiar o desempenho, uma vez que não é preciso criar um stack trace e, se for bem configurado, pode tornar o código mais seguro.

Exceções personalizadas

Também podes criar as tuas próprias exceções herdando da classe Exception. Ao fazê-lo, podes, opcionalmente, substituir o método initialize para definir a mensagem da exceção. Isto pode ser feito atribuindo a mensagem a uma variável de instância chamada @message.

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

raise MyException.new

Instruções

O dia que esperaste tanto tempo chegou finalmente e és agora o orgulhoso proprietário de uma bela quinta nos Alpes.

Continuas a não gostar de acordar demasiado cedo para alimentar as tuas vacas. Como és um excelente engenheiro, construis um distribuidor de comida, o FEED-M-ALL.

A última coisa de que precisas para terminar o teu projeto é um pedaço de código que calcule a quantidade de forragem que cada vaca deve receber. É importante que cada vaca receba a mesma quantidade, tens de evitar conflitos. As vacas são muito sensíveis.

Felizmente, não precisas de descobrir sozinho todas as fórmulas para calcular as quantidades de forragem. Usas uma biblioteca externa misteriosa que encontraste na internet. Supostamente, o resultado são as vacas mais felizes. A biblioteca expõe uma classe que cumpre a seguinte interface. Vais apoiar-te nisso no código que escreveres.

class FodderCalculator
  def fodder_amount! : Number 
  end

  def fattening_factor! : Number
  end
end

À medida que trabalhas no teu código, vais melhorar o tratamento de erros para o tornar mais robusto e mais fácil de depurar quando o usares no teu dia a dia na quinta.

1. Divide a comida de forma igual

Antes de mais, concentras-te em escrever o código necessário para calcular a quantidade de forragem por vaca.

Implementa um método TheFarm.divide_food que aceita como argumentos um objeto FodderCalculator e um número de vacas como inteiro. Para esta tarefa, assumes que o número de vacas recebido é sempre maior que zero. O método deve devolver a quantidade de comida por vaca.

Para fazer o cálculo, precisas primeiro de obter a quantidade total de forragem para todas as vacas. Fazes isso chamando o método fodder_amount! e passando o número de vacas. Além disso, precisas de um fator pelo qual essa quantidade tem de ser multiplicada. Obténs esse fator chamando o método fattening_factor!. Com estes dois valores e o número de vacas, podes agora calcular a quantidade de comida por vaca (como um float64).

Se um dos métodos que chamas devolver um erro, a execução deve parar e 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. Verifica o número de vacas

Enquanto trabalhavas na primeira tarefa acima, apercebeste-te de que a biblioteca externa que usas não tem tanta qualidade como pensavas. Por exemplo, não consegue lidar corretamente com valores de entrada inválidos. Queres contornar esta limitação acrescentando uma verificação do valor de entrada no teu próprio código.

Escreve uma função TheFarm.validate_input_and_divide_food com a mesma assinatura que a TheFarm.divide_food acima.

  • Se o número de vacas recebido for maior que 0, a função deve chamar TheFarm.divide_food e devolver o resultado dessa chamada.
  • Se o número de vacas for 0 ou menos, a função deve lançar um erro com a mensagem "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. Melhora o tratamento de erros

Verificar o número de vacas antes de o passar adiante foi uma boa jogada, mas não estás totalmente satisfeito com a mensagem de erro pouco específica. Decides fazer melhor, criando uma classe de erro personalizada chamada InvalidCowsError.

O erro personalizado deve guardar o número de vacas (Int32) e uma mensagem personalizada (String), e o método Expection deve serializar os dados no seguinte formato:

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

Com o teu erro personalizado em mãos, implementa uma função TheFarm.validate_number_of_cows que aceita o número de vacas como inteiro e devolve um erro (ou nil).

  • Se o número de vacas for menor que 0, a função devolve um InvalidCowsError com a mensagem personalizada definida como "there are no negative cows".
  • Se o número de vacas for 0, a função devolve um InvalidCowsError com a mensagem personalizada definida como "no cows don't need food".
  • Caso contrário, a função devolve nil para indicar que a validação foi bem-sucedida.
TheFarm.validate_number_of_cows(5)
# => nil

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

Depois do trabalho árduo de montar esta função de validação, reparas que já é noite e deixas a secretária para apreciar o pôr do sol sobre as montanhas. Deixas para outro dia a tarefa de acrescentar mesmo a nova função de validação ao teu código.

Editar via GitHub A ligação abre numa nova janela ou separador
Crystal Exercism

Estás pronto para começar a-quinta?

Inscreve-te no Exercism para aprenderes e dominares Crystal com 26 conceitos142 exercícios, e mentoria humana real, tudo grátis.