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_filter
、after_filter
でコールバックメソッドが定義できるようになる。
後は、Greeting
クラスの中で、それぞれのコールバックされるメソッドを記述し、コールバックメソッドの呼び出しを行なわないといけない。
Open Class で Greeting
クラスを再定義し、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 を使うこともできるが、より自由な組み込みができそう。