読者です 読者をやめる 読者になる 読者になる

gaaamiiのブログ

悪気なく間違ったことを書いている時があります。コメントやTwitter、ブコメなどでご指摘ください

「Effective Ruby」を読んで例外について学んでる

読んでる。

なんで例外の章を読むのか

Yomomoをえっさほいさちょっとずつ作りこんでいて、ああトップページもっときれいにしたいとかiPhoneアプリ欲しいとか、そういうキラキラしたことについてはすごい楽しく頻繁に考える。でもアプリケーションの不具合についてはだいたい見つかってから直したりしがちで、その直し方についてもその場しのぎっぽいものが多い。

はやくどうにかしなきゃと焦ってコミットしたときのやつ

やっぱりできる大人といえばみんな例外をわきまえてる(きっと)。これとこれとこれとこういう例外があり得ますよね、ということはそれをこういう順序でどこで待ち構えてれば大丈夫ですよね、というのが分かっていれば、しっかりした大人の職業プログラマーの道へと進めるんじゃないだろうか。

そういう動機で例外の章を読むことにした。

Effective Ruby

Effective Ruby

例外ってなんだ

そもそも例外ってなんだ。この本ではこう述べられている。

例外は、2つの異なる言語機能を1つにまとめたものだと考えられる。2つとは、エラーの説明と制御フローである。

なるほど。中身をなぞるだけではつまらないので、実際にyomomoの以前のコードを例として使いながらこの第5章の内容を追ってみる。

エラーの説明

著者は想定していないだろうけど世の中は広いので、例外をつかまえずエラーの説明をログに吐かない糞な個人開発者もいる。僕です。

しかしそんなやつでも使っているのがrescueだ。何も考えずに利用していたけれど、ここでしっかりと正しい理解を獲得しておきたい。

例外について考えたこともない人がrescueを使うとどうなるか

例外について考えたこともない人がrescueを使うとどうなるか、という悪い例がある。以下はYomomoでこの前まで使っていたメソッドだ。Amazon Product Advertising API へのアクセスに失敗した時に、試行回数の上限に達するまでアクセスを繰り返す。

  def get_books(asins, search_params, trial_limit = 2)
    trial = 0
    begin
      return Amazon::Ecs.item_lookup(asins, search_params).items
    rescue => e
      p e
      trial += 1
      retry if trial <= trial_limit
      return false
    end
  end

見ての通り、「エラーの説明」にあたる部分がない。「アクセスに失敗してそこで落ちてしまっては困るので、とりあえずもう二回まではやってみてね」というのをそのまま書いただけの状態だ。

ここで使っているamazon-ecsというgemにはちゃんと例外が用意されているので、それを使ってログを吐くようにしておくべきだ。

    rescue Amazon::RequestError => e
      log.error(e)
      trial += 1
      retry if trial <= trial_limit
      return false
    rescue => e
      log.error(e)
    end 

自分が書いた上記のような例ほどひどいものを書く人は少ないかもしれないけど、例外のクラスを指定せずに受け取って処理する以下のようなコードについては著者も懸念していて、これのまずさについて丁寧に説明している。

begin
  task.perform
rescue => e
  logger.error(e)
end

ご存知のように、このような形でrescueを使うのは、StandardErrorとそのあらゆるサブクラスを処理するという意味である。

例外の定義とraise

自分でクラスやモジュールを定義するようなときには、上でいうAmazon::RequestErrorのような例外をどう定義して、いつ発生させるかということについて考えなければいけない。

制御フロー

例外をどう使うか、どう定義するか、どう発生させるか、ということについてはわかった。じゃあ例外を捕まえた節で、制御フローをどう作っていけばいいのか。アプリケーションやらシステムを作る側としてはこちらの方が重大な問題だ。本章の項目26と27で、この問題について説明されている。

retry

これまた上で挙げたAmazon Product Advertising API へのアクセスの部分が例として使えそうなので、これの悪いところをベースに考えていく。

    begin
      return Amazon::Ecs.item_lookup(asins, search_params)
    rescue Amazon::RequestError => e
      log.error(e)
      trial += 1
      retry if trial <= trial_limit
      return false
    rescue => e
      log.error(e)
    end 

上でも書いたけど、ここでretryを入れた理由はAPIへのリクエストが不正だっつってエラーが起きた時に、そこで処理を終わらせないためだ。けれども、このretryの使い方にはいくつか問題がある。その問題や疑問について、この本が全て解消してくれた。

retry がよくわかってないから最初の処理でもreturnしてる

begin節で例外が吐かれなければretryしないので、最後に評価する式はAmazon::Ecs.item_lookup(asins, search_params)になる。だからreturnを明示的に書かないでもいいはず。

begin
  Amazon::Ecs.item_lookup(asins, search_params)
rescue
...
begin
  # なんかする
rescue
  # なんか失敗する
  retry
end

これは、以下に等しいらしい。

while true
  begin
    # なんかする
  rescue
    # なんか失敗する
  else
    break
  end
end

throw


書き途中です。