最近、Railsのソースコードを読むようにしています。Railsなんかは難しくてなかなか詳細つかみにくいですが、ActiveSupportのメソッドなんかはその中でも比較的分かりやすかったです。今回はattr_internalについて読んだのでそれについてメモしていきたいと思います。
attr_internalとは
Railsガイドにも説明がありましたが、分かりにくかったので自分なりに噛み砕いて書いていきます。
# ライブラリ
class ThirdPartyLibrary::Crawler
attr_internal :log_level
end
# クライアントコード
class MyCrawler < ThirdPartyLibrary::Crawler
attr_accessor :log_level
end
ライブラリの中で@log_levelの参照や書き込みができるように定義されています。このライブラリのクラスを継承したり、ミックスインして同じ名前のアクセサーを定義した場合どうなるでしょうか。クライアントコードの中で、開発者は知らず知らずの内にインスタンス変数を上書きして、ライブラリのコードを壊してしまう可能性があります。それを防いでくれるのがattr_internalです。attr_accesorと同じようにセッターとゲッターを定義しますが、中のインスタンス変数を@_log_levelというようにクライアントコード側で被らないように工夫がされます。では、ソースコードを見ていきます。
ソースコードを読む
attr_internalメソッドはactive_support/core_ext/module/attr_internal.rbにあります。
まず、attr_internalメソッドはattr_internal_accessorのエイリアスであることが分かります。
alias_method :attr_internal, :attr_internal_accessor
それではattr_internal_accessorメソッドを見てみます。
attr_internal_readerメソッドとattr_internal_writerメソッドを呼び出しているだけですね。
def attr_internal_accessor(*attrs)
attr_internal_reader(*attrs)
attr_internal_writer(*attrs)
end
まずは、attr_internal_readerメソッドからです。
ここではなにやら引数に渡された可変長引数をイテレートして、attr_internal_defineメソッドに渡しているようです。
def attr_internal_reader(*attrs)
attrs.each { |attr_name| attr_internal_define(attr_name, :reader) }
end
ここがメインの処理のようです。attr_nameには:log_levelが、typeには:readerが割り当てられています。
2行目でattr_internal_ivar_nameメソッドを呼び出していますね。
def attr_internal_define(attr_name, type)
internal_name = attr_internal_ivar_name(attr_name).delete_prefix("@")
# use native attr_* methods as they are faster on some Ruby implementations
public_send("attr_#{type}", internal_name)
attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
alias_method attr_name, internal_name
remove_method internal_name
end
attr_internal_ivar_nameメソッドでなにやらModuleの特異メソッドを呼び出しています。
def attr_internal_ivar_name(attr)
Module.attr_internal_naming_format % attr
end
特異メソッドの定義を見つけました。ここでselfを拡張してアクセサーを定義していますね。余談ですがクラスへの特異メソッドの定義はクラスメソッドの定義と同義なので、class << self; endがなくても動きます。
そして、アクセサーに”@_%s”を返すようにセットしています。
class << self
# この箇所はModuleの定義のときに呼ばれる
# selfはModule
attr_accessor :attr_internal_naming_format
end
self.attr_internal_naming_format = "@_%s"
つまり、Module.attr_internal_naming_format % attr
は@_log_levelを返します。あまり見慣れないですが、String#%を使っています。
p "i = %d" % 10 # => "i = 10"
p "i = %s" % 'hoge' # => "i = hoge""
ここまでで、もとのattr_nameの先頭に_をつけるところまで来ました。
次にpublic_send(~~)でModuleをレシーバとしてattr_readerメソッドを引数に_log_levelを指定して呼び出しています。そうして生えたinternal_nameメソッドをattr_nameという名前でメソッドをコピーして、最後にinternal_nameメソッド(_log_level)を削除しています。
public_send("attr_#{type}", internal_name)
attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
alias_method attr_name, internal_name
remove_method internal_name
こうすることで、log_levelというインターフェイスを残しつつ内部で使われるインスタンス変数は@_log_levelにするということを実現していたのですね。
attr_writerについてはほとんど同じなので割愛します。
さいごに
なにかライブラリをつくるときにはこのattr_internalメソッドを使わないと使う側で予期せぬバグが起きそうなので、積極的に使っていきたいと思いました。
ここまで読んでいただきありがとうございました。