メソッドを動的に定義する

ふと、メソッドを動的に定義したいなと思った。

find_record_by_id :diary

とかくと、以下のようなメソッドが定義されるイメージ。

def find_diary_by_id(id)
  if result = Diary.find_by_id(id)
    result
  else
    render :text => "無効な操作です。"
    false
  end
end

ApplicationControllerに次のようなメソッドを書けばいいけど、今回は練習のため。

def find_record_by_id(id,klass)
  if result = klass.find_by_id(id)
    result
  else
    render :text => "無効な操作です。"
    false
  end
end

動的にメソッドを定義する

まず最初に思ったのが、evalを使えばいいんじゃないかということ。
class Fuga
  class << self
    def method_define
      eval("def fuga;1;end")
    end
  end
end

Fuga.method_define 
#=> nil
Fuga.new.fuga
#=>NoMethodError: undefined method `fuga' for
        from (irb):11
irb(main):012:0> Fuga.fuga
#=> 1

だめだった。クラスメソッドになってしまう。クラスメソッド内で、evalしたんだから当然か。もともとのFugaクラスにメソッドが追加されずに、特異クラス(Singletonクラス)にメソッドが追加されるから、クラスメソッドになるのか。

instance_evalはどうかな
class Hoge
  class << self
    def method_define
      instance_eval("def hoge;1;end")
    end
  end
end

Hoge.method_define
#=> nil
Hoge.hoge
#=> 1

同じか。リファレンスを見てみると、次の様に書いてある。

  • instance_eval {|obj| ... }

オブジェクトのコンテキストで文字列 expr を評価してその結果を返します。
ブロックが与えられた場合にはそのブロックをオブジェクトのコンテキストで評価してその結果を返します。ブロックの引数 obj には self が渡されます。
オブジェクトのコンテキストで評価するとは self をそのオブジェクトにして実行するということです。また、文字列/ブロック中でメソッドを定義すれば self の特異メソッドが定義されます。


オブジェクトのコンテキストで評価するとは self を・・・のところから、上記の例の場合でも、クラスメソッドになってしまう。本当は、selfはHogeクラスでなく、Hogeクラスのインスタンスになって欲しい。つまり、Hogeクラスのコンテキストでevalが実行されて欲しい。(ここら辺が少しややこしい。まだ完全に理解してないorz)

class_evalで試してみる

リファレンスを見てみると。

  • module_eval

モジュールのコンテキストで文字列 expr を評価してその結果を返します。
モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。つまり、そのモジュールの定義文の中にあるかのように実行されます。


これだ。

class Fuga
  class << self
    def method_define
      class_eval("def fuga;1;end")
    end
  end
end

Fuga.method_define
Fuga.fuga
#=>NoMethodError: undefined method `fuga' for Fuga:Class
        from (irb):9
Fuga.new.fuga
#=>1 

find_record_by_id

実際のRailsのコードではないが、こんな感じになる。

class Diary
  class << self
    def find_by_id(id)
      "Return Diary Record id=#{id}"
    end
  end
end

class ApplicationController
  class << self
    def find_record_by_id(*args)
      args.each do |arg|
        arg = arg.to_s
        class_eval <<-METHOD
          def find_#{arg}_by_id(id)
            #{arg.capitalize}.find_by_id(id)
          end
        METHOD
      end
    end
  end
end

class DiaryController < ApplicationController
  find_record_by_id :diary
  
  def show
    diary = find_diary_by_id(10)
  end
end

p DiaryController.new.show
#=>"Return Diary Record id=10"

define_methodっていうメソッドがあった(i _ i)

これを使って、上記のApplicationController::find_record_by_idを書いてみる。

def find_record_by_id(*args)
  args.each do |arg|
    arg = arg.to_s
    define_method("find_#{arg}_by_id") do |id|
      eval("#{arg.capitalize}").find_by_id(id)
    end
  end
end

ん。逆にややこしい気がする(笑)