メソッドの実行でクラスメソッドが追加される
以下は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の実装も気になりましたが、今回はこの辺で。
環境
- OS : Mac OS X バージョン 10.8.4
- Ruby バージョン 2.3.1
- Ruby on Rails バージョン 4.2.6