Ruby on Rails のソースコードを読む Active Support編
目的
ruby 及び rais の理解を深める為、読んだソースコードの内容をメモしていきたいと思います。
この記事で説明するメソッド名と役割
- cattr_accessor
- cattr_reader
- cattr_writer
クラス変数(@@で始まる変数)のアクセサを定義。
- mattr_accessor
- mattr_reader
- mattr_writer
モジュール変数(@@で始まる変数)のアクセサを定義。
※アクセサとは・・・JavaやC系でいうところのgetter/setter
ソースコード
コメントを削除したものを載せます。
全部で57行!
なんとか読めそうな気がしますね。
require 'active_support/core_ext/array/extract_options'
class Module
def mattr_reader(*syms)
options = syms.extract_options!
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
def self.#{sym}
@@#{sym}
end
EOS
unless options[:instance_reader] == false || options[:instance_accessor] == false
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}
@@#{sym}
end
EOS
end
class_variable_set("@@#{sym}", yield) if block_given?
end
end
alias :cattr_reader :mattr_reader
def mattr_writer(*syms)
options = syms.extract_options!
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
def self.#{sym}=(obj)
@@#{sym} = obj
end
EOS
unless options[:instance_writer] == false || options[:instance_accessor] == false
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}=(obj)
@@#{sym} = obj
end
EOS
end
send("#{sym}=", yield) if block_given?
end
end
alias :cattr_writer :mattr_writer
def mattr_accessor(*syms, &blk)
mattr_reader(*syms, &blk)
mattr_writer(*syms, &blk)
end
alias :cattr_accessor :mattr_accessor
end
ソースコードをざっと見た感じ・・・
mattr_reader と mattr_writer の内容はほぼ同じような感じがしますね。
また、mattr_accessor は mattr_reader と mattr_writer を呼び出しているようです。
ポイント1
options = syms.extract_options!
mattr_reader と mattr_writer でそれぞれ宣言されています。
内容は以下のファイルに宣言されています。
class Hash
def extractable_options?
instance_of?(Hash)
end
end
class Array
def extract_options!
if last.is_a?(Hash) && last.extractable_options?
pop
else
{}
end
end
end
レシーバのオブジェクトが引数klassクラスのインスタンスであればtrue、そうでなければfalseを返します。
is_a?メソッドまたはkind_of?メソッドは、レシーバのオブジェクトが引数klassクラスのインスタンスであればtrue、そうでなければfalseを返します。
つまり、引数として渡された配列の最後をチェックして、Hashだったら取り出して変数 options に格納しています。
ポイント2
raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
アクセサとして使用可能な名前かチェックしています。
ポイント3
このメソッドのメイン処理とも言える部分です。
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
def self.#{sym}
@@#{sym}
end
EOS
class_evalメソッドの引数に文字列codeを渡すと、その文字列をクラス定義やモジュール定義の中のコードであるように実行します。
FILE は実行ファイルのファイルパスを取得し、LINE は定義されているメソッドの行番号を取得します。
つまり、アクセサを指定したファイルの次の行に、アクセサを定義しています!
class_eval、とても強力なメソッドですね・・・!
unless options[:instance_reader] == false || options[:instance_accessor] == false
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}
@@#{sym}
end
EOS
end
ポイント1で用意した変数 options を参照しています。
readerインスタンスメソッドの生成を制御しています。
ここまでが mattr_reader(所謂getter) のソースコードになります。
ポイント4
mattr_writer(所謂setter)も同様に、class_evalメソッドを使用してアクセサを定義しています。
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
def self.#{sym}=(obj)
@@#{sym} = obj
end
EOS
unless options[:instance_writer] == false || options[:instance_accessor] == false
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}=(obj)
@@#{sym} = obj
end
EOS
end
最後に
rails でアクセサを定義した際、どのような処理が実行されているのかをつかむことができました。
また class_eval メソッドの存在を初めて知り、他のメソッドの実装も見てみたくなりました。