農場

農場

学習演習

はじめに

理想の世界では、すべてが完璧に動きます。 しかし現実の世界では、物事がうまくいかないこともあり、こうした状況にどう対処するかが、ソフトウェアを堅牢で信頼できるものにするために重要になります。 例外は、エラーや予期しない状況にうまく対処するための、プログラミングにおける重要な概念です。

エラーを発生させても、それを処理しなければ、プログラムは停止し、エラーメッセージを出力します。 ほとんどの場合、エラーが起きたときにプログラムを停止させたくはありません。むしろ、エラーを処理してプログラムの実行を続けたいはずです。

例外を発生させる

Crystalでは、例外はraiseキーワードを使って発生させます。raiseにはStringまたはExceptionオブジェクトを渡すことができます。 処理しなければ、プログラムは停止し、エラーメッセージを出力します。

raise "This is an error"

Crystalには、ArgumentError、IndexError、KeyError、IOError、SystemCallError、TypeError、ZeroDivisionErrorなど、多数の組み込み例外があります。 これらには、例外にメッセージを渡す必要があります。

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

例外の処理

例外が発生したときにプログラムがクラッシュしてほしくはありません。 ですから、エラーが起きやすいとわかっているコードは、beginブロックで囲み、rescueブロックで例外を捕まえることができます。 beginブロックは例外が発生する可能性のあるコードの始まりを示し、rescueブロックが例外を処理します。

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

rescueブロックには変数を指定して、例外オブジェクトを受け取ることもできます。

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

rescueブロックには特定の例外の型を指定して、その例外だけを捕まえることもできます。

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

複数のrescueブロックを使って、さまざまな種類の例外を処理できます。 次の例では、最初のrescueブロックがArgumentErrorを捕まえ、2番目のrescueブロックがあらゆるその他の例外を捕まえます。

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

beginブロックにはelseブロックを付けることもでき、これは例外が発生しなかったときに実行されます。

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

最後に、例外が発生したかどうかに関係なく、必ず実行されるensureブロックがあります。

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

メソッドの慣習

メソッドの中には、!が付くものと付かないものの2つの版があるものがあります。 これには2つの意味がありえます。1つはメソッドがオブジェクトを変更すること、もう1つはメソッドが例外を発生させる可能性があることです。

ただし、真偽値の概念で触れた、メソッド名を?で終えるという別の慣習もあります。 メソッドの中には、デフォルトでは例外を発生させますが、?で終わる版もあり、そちらは例外を発生させる代わりにnilを返します。

エラーの発生を避けたいときには、これが理想的です。 スタックトレースを生成する必要がないためパフォーマンスにも有利で、正しく設定すればコードをより安全にすることもできます。

独自の例外

Exceptionクラスを継承して、独自の例外を作ることもできます。 その際、initializeメソッドをオーバーライドして、例外のメッセージを設定することもできます。 これは、@messageという名前のインスタンス変数にメッセージを代入することで行えます。

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

raise MyException.new

説明

待ちに待った日がついにやってきて、今ではアルプスにある美しい農場の誇らしい持ち主です。

それでも、朝早く起きて牛に餌をやるのはやっぱり好きになれません。 優秀なエンジニアである皆さんは、餌やり機FEED-M-ALLを作ります。

プロジェクトを完成させるために最後に必要なのは、それぞれの牛が受け取るべき餌の量を計算するコードです。 どの牛も同じ量を受け取ることが大切で、争いが起きないようにする必要があります。 牛はとても繊細なのです。

幸いなことに、餌の量を計算する式をすべて自分で導き出す必要はありません。 インターネットで見つけた、謎の外部ライブラリを使います。 これを使えば、牛たちは最高に幸せになるはずです。 そのライブラリは、次のインターフェースを満たすクラスを公開しています。 自分で書くコードでは、これを前提にします。

class FodderCalculator
  def fodder_amount! : Number 
  end

  def fattening_factor! : Number
  end
end

コードを書いていく中で、エラー処理を改善し、より堅牢にして、農場での日々の生活で使うときに後からデバッグしやすくします。

1. 餌を均等に分ける

まずは、牛1頭あたりの餌の量を計算するために必要なコードの作成に取り組みます。

FodderCalculatorオブジェクトと、牛の頭数を表す整数を引数に取るメソッドTheFarm.divide_foodを実装しましょう。 このタスクでは、渡される牛の頭数は常に0より大きいと仮定します。 このメソッドは、牛1頭あたりの餌の量を返すようにします。

計算するには、まずすべての牛の餌の合計量を取得する必要があります。 これは、fodder_amount!メソッドを呼び出し、牛の頭数を渡すことで行います。 さらに、この量に掛け合わせる係数も必要です。 この係数は、fattening_factor!メソッドを呼び出すことで取得します。 これら2つの値と牛の頭数がそろえば、牛1頭あたりの餌の量を(float64として)計算できます。

呼び出したメソッドのどれかがエラーを返した場合、処理を中断して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. 牛の頭数を確認する

上の最初のタスクに取り組む中で、使っている外部ライブラリが思っていたほど高品質ではないことに気づきました。 たとえば、無効な入力を適切に扱えません。 この制限を回避するために、自分のコードで入力値のチェックを追加することにします。

上記のTheFarm.divide_foodと同じシグネチャを持つ関数TheFarm.validate_input_and_divide_foodを書きましょう。

  • 渡された牛の頭数が0より大きい場合、関数はTheFarm.divide_foodを呼び出し、その呼び出しの結果を返します。
  • 牛の頭数が0以下の場合、関数はメッセージ"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. エラー処理を改善する

渡す前に牛の頭数を確認するのは良い判断でしたが、具体的でないエラーメッセージにはあまり満足していません。 もっと良い方法を取ることにし、InvalidCowsErrorというカスタムエラークラスを作成します。

このカスタムエラーは牛の頭数(Int32)とカスタムメッセージ(String)を保持し、Expectionメソッドはデータを次の形式でシリアライズするようにします。

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

カスタムエラーを手に入れたら、牛の頭数を整数として受け取り、エラー(またはnil)を返す関数TheFarm.validate_number_of_cowsを実装しましょう。

  • 牛の頭数が0より小さい場合、関数はカスタムメッセージを"there are no negative cows"に設定したInvalidCowsErrorを返します。
  • 牛の頭数が0の場合、関数はカスタムメッセージを"no cows don't need food"に設定したInvalidCowsErrorを返します。
  • それ以外の場合、関数は検証が成功したことを示すためにnilを返します。
TheFarm.validate_number_of_cows(5)
# => nil

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

この検証関数を設定するという大変な作業を終えたあと、もう夕方になっていることに気づき、山の上に沈む夕日を楽しむために机を離れます。 新しい検証関数を実際に自分のコードに追加する作業は、また別の日に回すことにします。

GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Crystal Exercism

農場を始める準備はできましたか?

Exercismに登録すれば、26個のコンセプト142個の演習、そして本物の人間によるメンタリングとともに、Crystalを学んでマスターできます。すべて無料です。