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

written by taka on December 31st, 2009 @ 07:42 PM  

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

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

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

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

1
2
3
4
5
6
7
8
9
class Greeting
  def initialize(out = STDOUT)
    @out = out
  end

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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!」の前に表示される
# >> hello!
# >> bye!     # 「hello!」の後に表示される

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 のコントローラのフィルタでもこのモジュールが利用されている。拝借する。

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

1
2
3
4
5
6
7
8
9
10
11
12
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 メソッドに機能を追加する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 既存のクラス
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

# Greetin クラスを再定義
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

  # ActiveSupport::CoreExtensions::Module#alias_method_chain を使えば1行で書ける
  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

# テスト
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

テストを実行。

1
2
3
4
5
6
7
$ ruby filter_example1.rb
Loaded suite filter_example1
Started
.
Finished in 0.000389 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

OK だ。

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

Post a comment