Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

動的例外マッチャーでRubyのrescueをレベルアップさせる

Rubyでrescue節を使う際、どの種類の例外を捕捉するかを指定できます。方法はシンプルで、例外クラスのリストを渡すだけです。

begin
  raise RuntimeError
rescue RuntimeError, NoMethodError
  puts "rescued!"
end

しかし、コードを書いている時点で、実際にどの例外クラスが発生するか分からない場合はどうでしょうか?最も分かりやすい解決策は、すべての例外を捕捉し、何らかの判定を行った上で、条件を満たさない例外を再スローすることです。例えば、次のようなコードです。

begin
  raise "FUBAR! The ship's going down!"
rescue => e
  raise unless e.message =~ /^FUBAR/
  ... do something ...
end

しかし、これは少し退屈ですよね。しかも、DRY原則から外れた書き方でもあります。rescue節に「この条件に合う例外だけを捕捉してほしい」と直接指示できれば、もっとエレガントなはずです。そして、これがRubyなのですから、もちろんそれが可能です!

rescueが例外をマッチングする仕組み

rescueブロック内で例外が発生すると、Rubyインタプリタは、発生した例外のクラスを、指定された例外クラスのリストと照合します。一致が見つかれば、その例外は捕捉されます。

内部的なマッチング処理は、イメージとしては次のようなものです。

exception_classes_to_rescue.any? do |c|
  c === raised_exception.class
end

Rubyの他のあらゆる演算子と同じように、===も単なるメソッドにすぎません。この場合、cのメソッドです。では、独自の===メソッドを定義したら、何が起こるのでしょうか?

次の例では、Anything === xがどんな値のxに対してもtrueを返すようなAnythingクラスを作成しています。このクラスをrescueの引数として渡すと、すべての例外が捕捉されることになります。

class Anything
  def self.===(exception)
    true
  end
end

begin
  raise EOFError
rescue Anything
  puts "It rescues ANYTHING!"
end

すべての例外を捕捉するだけなら、もっと良い方法はいくらでもあります。しかし、このコードには重要な示唆が含まれています。

  1. ===を実装していれば、Exceptionを継承していないクラスでもrescue節に渡せる

  2. ===を制御できれば、どの例外を捕捉するかを思い通りに制御できる

メッセージパターンで例外を捕捉する

ここまでの仕組みが分かれば、例外のメッセージが特定のパターンに一致する場合のみ捕捉するコードは、簡単に書けるはずです。

class AllFoobarErrors
  def self.===(exception)
    # メッセージがFOOBARで始まる例外をすべて捕捉
    exception.message =~ /^FOOBAR/
  end
end

begin
  raise EOFError, "FOOBAR: there was an eof!"
rescue AllFoobarErrors
  puts "rescued!"
end

カスタム属性で例外を捕捉する

例外オブジェクトそのものにアクセスできるため、マッチャーはオブジェクト内部のあらゆるデータを利用できます。

たとえば、「severity(重大度)」というカスタム属性を持つ例外があるとしましょう。「低い重大度」の例外は握りつぶしたい一方で、「高い重大度」の例外はそのまま通過させたい。そんな場合、次のように実装できます。

class Infraction < StandardError
  attr_reader :severity
  def initialize(severity)
    @severity = severity
  end
end

class LowSeverityInfractions
  def self.===(exception)
    exception.is_a?(Infraction) && exception.severity == :low
  end
end

begin
  raise Infraction.new(:low)
rescue LowSeverityInfractions
  puts "rescued!"
end

マッチャーを動的に生成する

ここまでの内容は十分にクールですが、ボイラープレートコードが多くなるのが難点です。マッチャーの種類ごとに個別のクラスを手作業で定義するのは、いかにも冗長です。幸い、少しのメタプログラミングを駆使すれば、これをかなりDRYにまとめられます。

次の例では、マッチャークラスを自動生成するメソッドを定義しています。ブロックとしてマッチングロジックを渡すと、そのブロックを===メソッドの内部で使用する新しいクラスが生成されます。

def exceptions_matching(&block)
  Class.new do
    def self.===(other)
      @block.call(other)
    end
  end.tap do |c|
    c.instance_variable_set(:@block, block)
  end
end

begin
  raise "FOOBAR: We're all doomed!"
rescue exceptions_matching { |e| e.message =~ /^FOOBAR/ }
  puts "rescued!"
end

留意点

Rubyの数々のクールなトリックと同じように、これが「狂気の産物」なのか「優れたアイデア」なのか、筆者自身まだ判断しかねています。おそらく、両方なのでしょう。このテクニックを第一の選択肢にすることはおすすめしませんが、先ほどのような重大度に応じて例外を捕捉したい場面では、確かに役立ちそうです。いずれにせよ、あなたの道具箱に加わる、もうひとつのツールになるはずです!

  1. Pryで例外をスマートに扱う方法|バックトレース表示からカスタマイズまで

    Rails開発者なら、Railsコンソールを頻繁に使う方も多いのではないでしょうか。そして今や、RailsコンソールといえばPryが定番。素のIRBと比べると、その使い勝手は段違いです。 実はPryには、例外を扱う際に非常に便利な機能が標準で組み込まれています。この記事では、その中でも特に役立つ3つの機能を紹介します。 直近の例外の完全なバックトレースを表示する Pry(IRBでも同様)で例外が発生すると、通常は短縮されたバックトレースが表示されます。多くの場合それで十分ですが、詳細を確認したい場面もありますよね。 そんなときは wtf -v コマンドを使いましょう。直近の例外の完全な

  2. Windows 10の「ダイナミックロック」機能でPCを自動的に保護する方法

    Windows 10に追加された新機能のひとつに「ダイナミックロック(Dynamic Lock)」があります。これはクリエイターズアップデート(Creators Update)で搭載された、やや隠れた存在の機能であり、その便利さを知らないユーザーは今でも少なくありません。 この記事では、ダイナミックロックとは何か、そして実際にどう使えばよいのかをわかりやすく解説します。 Windows 10の「ダイナミックロック」とは? ダイナミックロックは、MicrosoftがWindows 10に追加したセキュリティ機能です。Bluetoothを利用して、デスクトップPCやノートPCを自動的にロックできる