A fazenda

A fazenda

Exercício de aprendizagem

Introdução

No mundo ideal, tudo funciona perfeitamente. Mas no mundo real, as coisas podem dar errado, e a forma como lidamos com essas situações é importante para garantir que nosso software seja robusto e confiável. As exceções são um conceito crucial na programação que nos permite lidar com erros e situações inesperadas de forma elegante.

Lançar um erro, se não for tratado, interrompe o programa e exibe uma mensagem de erro. Na maioria dos casos, você não quer que seu programa pare quando ocorre um erro; em vez disso, você quer tratar o erro e continuar executando o programa.

Lançando 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 embutidas no Crystal, como ArgumentError, IndexError, KeyError, IOError, SystemCallError, TypeError, ZeroDivisionError e muitas outras. Essas exigem que você passe uma mensagem para a exceção.

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

Tratando exceções

Não queremos que nosso programa quebre quando uma exceção é lançada. Por isso, quando sabemos que um trecho de código é propenso a erros, podemos envolvê-lo em um 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 especificar 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 especificar um tipo de exceção específico, para capturar apenas aquela 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

Vários blocos rescue podem ser usados para tratar tipos diferentes de exceções. No exemplo abaixo, o primeiro bloco rescue vai capturar um ArgumentError, e o segundo bloco rescue vai capturar 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 nenhuma exceção for lançada.

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

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

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

Convenções de métodos

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

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

Isso é ideal quando você quer evitar que um erro seja lançado. Isso pode beneficiar o desempenho, já que não é preciso criar um stacktrace e, se configurado corretamente, pode deixar o código mais seguro.

Exceções personalizadas

Você também pode criar suas próprias exceções herdando da classe Exception. Ao fazer isso, você pode, opcionalmente, sobrescrever o método initialize para definir a mensagem da exceção. Isso 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 você tanto esperou finalmente chegou, e agora você é o orgulhoso dono de uma bela fazenda nos Alpes.

Você ainda não gosta de acordar cedo demais para alimentar suas vacas. Como você é um excelente engenheiro, você construiu um dispensador de comida, o FEED-M-ALL.

A última coisa necessária para concluir seu projeto é um trecho de código que calcule a quantidade de ração que cada vaca deve receber. É importante que cada vaca receba a mesma quantidade, você precisa evitar conflitos. Vacas são muito sensíveis.

Felizmente, você não precisa elaborar todas as fórmulas para calcular as quantidades de ração sozinho. Você usa uma biblioteca externa misteriosa que encontrou na internet. Ela deveria resultar nas vacas mais felizes. A biblioteca expõe uma classe que atende à seguinte interface. Você vai contar com isso no código que escrever.

class FodderCalculator
  def fodder_amount! : Number 
  end

  def fattening_factor! : Number
  end
end

À medida que você trabalha no seu código, vai melhorar o tratamento de erros para torná-lo mais robusto e mais fácil de depurar depois, quando você o usar no seu dia a dia na fazenda.

1. Divida a comida igualmente

Primeiro, você foca em escrever o código necessário para calcular a quantidade de ração por vaca.

Implemente um método TheFarm.divide_food que aceita como argumentos um objeto FodderCalculator e um número de vacas como um inteiro. Para esta tarefa, você assume que o número de vacas passado é sempre maior que zero. O método deve retornar a quantidade de comida por vaca.

Para fazer o cálculo, primeiro você precisa obter a quantidade total de ração para todas as vacas. Isso é feito chamando o método fodder_amount! e passando o número de vacas. Além disso, você precisa de um fator pelo qual essa quantidade precisa ser multiplicada. Você obtém esse fator chamando o método fattening_factor!. Com esses dois valores e o número de vacas, agora você pode calcular a quantidade de comida por vaca (como um float64).

Se um dos métodos que você chama retornar um erro, a execução deve parar e retornar 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. Verifique o número de vacas

Enquanto trabalhava na primeira tarefa acima, você percebeu que a biblioteca externa que usa não é tão de alta qualidade quanto você pensava. Por exemplo, ela não consegue lidar adequadamente com entradas inválidas. Você quer contornar essa limitação adicionando uma verificação do valor de entrada no seu próprio código.

Escreva uma função TheFarm.validate_input_and_divide_food que tem a mesma assinatura que a TheFarm.divide_food acima.

  • Se o número de vacas passado for maior que 0, a função deve chamar TheFarm.divide_food e retornar 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. Melhore o tratamento de erros

Verificar o número de vacas antes de passá-lo adiante foi uma boa jogada, mas você não está muito satisfeito com a mensagem de erro genérica. Você decide fazer melhor criando uma classe de erro personalizada chamada InvalidCowsError.

O erro personalizado deve armazenar 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}

Agora que você tem seu erro personalizado, implemente uma função TheFarm.validate_number_of_cows que aceita o número de vacas como um inteiro e retorna um erro (ou nil).

  • Se o número de vacas for menor que 0, a função retorna um InvalidCowsError com a mensagem personalizada definida como "there are no negative cows".
  • Se o número de vacas for 0, a função retorna um InvalidCowsError com a mensagem personalizada definida como "no cows don't need food".
  • Caso contrário, a função retorna 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 duro de configurar essa função de validação, você percebe que já é noite e sai da sua mesa para apreciar o pôr do sol sobre as montanhas. Você deixa para outro dia a tarefa de de fato adicionar a nova função de validação ao seu código.

Editar via GitHub O link abre em uma nova janela ou aba
Crystal Exercism

Tudo pronto para começar A fazenda?

Crie sua conta no Exercism para aprender e dominar Crystal com 26 conceitos142 exercícios e mentoria humana de verdade, tudo de graça.