メソッドの実行でクラスメソッドが追加される
以下は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)
singleton_class.define_method name, -> { puts "This metho 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
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の実装も気になりましたが、今回はこの辺で。
環境