Rails のコンローラのフィルタのようなものを実装してみる

Written by @dr_taka_n at 2009/12/31 19:42 []

Rails のコントローラで利用できるフィルターのような機能を Rails ではないアプリに組み込みたく、実装してみることにした。

ちなみに、Rails のコントローラ内で利用できるフィルタには、以下のものがある。

  • before_filter: アクションの前に実行
  • after_filter : アクションの後に実行
  • around_filter: アクションの前後で実行

あまりよい例を出せないのだが、現在以下のクラスが既に実装されているものとする。

class Greeting
  def initialize(out = STDOUT)
    @out = out
  end

  def hello
    @out.puts 'hello!'
  end
end

Rails のフィルターのように上記の hello メソッドの前後で実行するための機能をClass Macroで追加できるようにしたい。 実装イメージとしては、以下のような感じ。

class Greeting
  ...
  before_filter :before_greeting
  after_filter :after_greeting

  ...

  private
  def before_greeting
    @out.puts 'Ya!'
  end

  def after_greeting
    @out.puts 'bye!'
  end
end

Greeting.new.hello
# >> Ya!
# >> hello!
# >> bye!

通したいテストは以下のような感じ。

require 'test/unit'

class GreetingTest < Test::Unit::TestCase
  def setup
    @out = StringIO.new
    @greeting = Greeting.new(@out)
  end

  def test_running_filters
    @greeting.hello
    assert_equal ['Ya!', 'hello!', 'bye!'], @out.string.split("\n")
  end
end

やることは、2つ。

  • 既に実装されているGreeting#helloメソッドには手を入れないで機能を追加する。
  • 前後に呼び出す機能をコールバックとして定義し、実行できるようにする。

全て1から実装するのはシンドイので。。なにかと便利な ActiveSupport を使う。

ActiveSupport には、ActiveSupport::Callbacks というモジュールがあり、Rails のコントローラのフィルタでもこのモジュールが利用されている。拝借する。

まずは、前後に挟む機能を定義するためのフィルタの準備。モジュールで準備する。

require 'rubygems'
require 'active_support'

module Filters
  def self.included(base)
    base.class_eval do
      include ActiveSupport::Callbacks

      define_callbacks :before_filter, :after_filter
    end
  end
end

このモジュールは、利用するクラスでインクルードされると、

  • ActiveSupport::Callbacks モジュールをインクルードする。
  • ActiveSupport::Callbacks モジュールで定義されている define_callbacks メソッドで :before_filter:after_filter をコールバックメソッドとして定義する。

ということを行う。

これで、Greeting クラスで Filters モジュールをインクルードすれば、before_filterafter_filter でコールバックメソッドが定義できるようになる。

後は、Greeting クラスの中で、それぞれのコールバックされるメソッドを記述し、コールバックメソッドの呼び出しを行なわないといけない。

Open ClassGreeting クラスを再定義し、Around Alias で既にある hello メソッドに機能を追加する。

class Greeting
  include Filters

  before_filter :before_greeting
  after_filter :after_greeting

  def hello_with_filters
    run_callbacks(:before_filter)
    hello_without_filters
    run_callbacks(:after_filter)
  end

  alias_method :hello_without_filters, :hello
  alias_method :hello, :hello_with_filters

  private
  def before_greeting
    @out.puts 'Ya!'
  end

  def after_greeting
    @out.puts 'bye!'
  end
end

最終的に1つのファイルに以下のように記述している。

filter_example1.rb:

class Greeting
  def initialize(out = STDOUT)
    @out = out
  end
  
  def hello
    @out.puts 'hello!'
  end
end

require 'rubygems'
require 'active_support'

module Filters
  def self.included(base)
    base.class_eval do
      include ActiveSupport::Callbacks

      define_callbacks :before_filter, :after_filter
    end
  end
end

# Open Greeting class
class Greeting
  include Filters

  before_filter :before_greeting
  after_filter :after_greeting

  def hello_with_filters
    run_callbacks(:before_filter)
    hello_without_filters
    run_callbacks(:after_filter)
  end

  alias_method :hello_without_filters, :hello
  alias_method :hello, :hello_with_filters

  private
  def before_greeting
    @out.puts 'Ya!'
  end

  def after_greeting
    @out.puts 'bye!'
  end
end

# test
require 'test/unit'

class GreetingTest < Test::Unit::TestCase
  def setup
    @out = StringIO.new
    @greeting = Greeting.new(@out)
  end

  def test_running_filters
    @greeting.hello
    assert_equal ['Ya!', 'hello!', 'bye!'], @out.string.split("\n")
  end
end

テストを実行。

$ ruby filter_example1.rb
Loaded suite filter_example1
Started
.
Finished in 0.000389 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

OK だ。

このくらいのことであれば、AspectR を使うこともできるが、より自由な組み込みができそう。

blog comments powered by Disqus