yucatio@システムエンジニア

趣味で作ったものいろいろ

Rubyでの動的なクラスメソッドの追加方法

メソッドの実行でクラスメソッドが追加される

以下はrailsのscope定義文です。このように書くと、Book.costlyでレコードが取得できます。 メソッド呼び出しでメソッドが動的に追加されます。 メソッドの引数が新しいメソッドのメソッド名になるのです。不思議!Javaではこのようなことはできません。

class Book < ActiveRecord::Base
  scope :costly, -> { where("price > ?", 3000) }
end

どのような方法でメソッドを追加できるのでしょうか。

singleton_class とdefine_methodでクラスメソッドを追加する

Rubyでは動的なメソッド追加は割と普通に行われるようで、define_methodで行うことができます。 クラスメソッドの追加は特異クラスへのメソッド追加として行うことができます。

class Sample
  def self.my_define_method(name)
    singleton_class.send(:define_method, name, -> { puts "This method name is '#{name}'" })
  end

  # クラス内でクラスメソッドを追加
  my_define_method :sample_method
end

# クラス外からクラスメソッドを追加
Sample.my_define_method :sample_method2

# 追加したクラスメソッドを実行
Sample.sample_method
Sample.sample_method2

これを実行すると、以下のように表示され、メソッドの追加に成功したことが分かります

$ ruby sample_01.rb 
This method name is 'sample_method'
This method name is 'sample_method2'

moduleをextendする方法

上記の方法で動的にクラスメソッドを追加することができました。 Ruby on Railsのscopeのコード*1を見ると、Moduleのコードを呼んでいるので、同じような実装を行ってみます。

module ParentModule
  def my_define_method(name)
    singleton_class.send(:define_method, name, -> { puts "This method name is '#{name}'" })
  end
end

class Sample
  extend ParentModule
  
  my_define_method :sample_method
end

Sample.sample_method

実行します。

$ ruby sample_02.rb 
This method name is 'sample_method'

メソッドの追加ができました。Railsのコードに近い形になりました。

クラス定義内でモジュールをextendすることで、モジュールのメソッドを特異メソッド(クラスメソッド)として取り込むことができます。

うまく行かなかった方法たち

define_methodをsingleton_classをレシーバとして呼び出した場合

priveteなメソッドと呼ぶなと怒られます。

class Sample
  def self.my_define_method(name)
    # エラー。priveteメソッドは呼び出せない
    singleton_class.define_method name, -> { puts "This metho name is #{name}" }
    # 正しくは下記
    # singleton_class.send(:define_method, name, -> { puts "This method name is '#{name}'" })
  end
end

# クラスメソッドを追加
Sample.my_define_method :sample_method

# 追加したクラスメソッドを実行
Sample.sample_method
$ ruby sample_01_bad.rb 
sample_01.rb:3:in `my_define_method': private method `define_method' called for #<Class:Sample> (NoMethodError)

singleton_class(オブジェクトの特異クラス)のdefine_methodはプライベートメソッドなので外部から呼び出せません。

ただし、sendメソッドを使用することでprivateメソッドを(強制的に)呼び出すことができます。

define_methodをインスタンスメソッドから実行した場合

インスタンスメソッドとして定義した場合は、そのインスタンスの特異メソッドとして定義され、クラスメソッドにはなりません。

class Sample
  def my_define_method(name)
    singleton_class.send(:define_method, name, -> { puts "This method name is '#{name}'" })
  end
end

instance = Sample.new
# インスタンスのメソッドを追加 
instance.my_define_method :sample_method

# インスタンスメソッドとして実行
instance.sample_method
# エラー。クラスメソッドとしては定義されていない
Sample.sample_method
$ ruby sample_01.rb 
This method name is 'sample_method'
sample_01.rb:14:in `<main>': undefined method `sample_method' for Sample:Class (NoMethodError)

インスタンスの特異クラス(singleton_class)にメソッドを追加したため、インスタンスにメソッドが追加されました。

後記

define_methodの実装も気になりましたが、今回はこの辺で。

環境