Rubyでメソッド名からメソッド定義を追加する方法

ライブラリを作成していてちょっとハマったのでメモ。

インスタンスメソッドを追加する

class Clazz
end

def add_instance_method(method_name)
  Clazz.__send__ :define_method, method_name do
    # メソッドの中身
    "#{method_name} called"
  end
end

add_instance_method("aaa")
Clazz.new.aaa # => "aaa called"

add_instance_method("bbb")
Clazz.new.bbb # => "bbb called"

これはおk。マニュアルにも書いてある。define_methodはプライベートなのでsendで呼び出す必要がある。

sendの代わりにclass_evalでも良い。←1.9ではこっちでやる。

クラスメソッドを追加する

これではまった。

やり方
class Clazz
end

def add_class_method(method_name)
  Clazz.class_eval do
    class << self; self; end.__send__ :define_method, method_name do
      # メソッドの中身
      "#{method_name} called"
    end
  end
end

add_class_method("ccc")
Clazz.ccc # => "ccc called"
解説

キモになるのは、

class << self; self; end.__send__ :define_method, method_name

の部分。これは、

class << self
  self
end

っていうクラスメソッドを追加する時に使うやつを一行で書いてるだけなんだけど、なにをやってるかというと、
class << self 〜 endのブロックでselfを戻している。

じゃ、selfは何かというとClassクラスのインスタンス、つまり対象のクラスに特異メソッドを追加する特異なクラスになっているらしい。調べ方が悪いのか名称は分からなかったのだけどclass << selfブロック内のselfが特異なクラスになっているから

Clazz
  class << self
    def ddd
    end
  end
end

とすると、Clazz.dddが追加されるということになっているらしい。同じようにselfに対してdefine_methodしてあげればクラスメソッドを追加することが出来る。実はARでたまに見るテクニックだったりした。

ちなみに、

def add_class_method(method_name)
  Clazz.class_eval do
    class << self
      define_method method_name do
      end
    end
  end
end

のようにしてもダメ。class << selfはクロージャじゃないのでmethod_nameには触れない。これでハマった。