普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました
読めるようにするまで
以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com
読んだ箇所
attr_internal
を今日は読んでみようと思います
どんな使い方だっけ?
読んでみる前にまずは使い方を調べてみます
RAILS GUIDESの日本語ドキュメントを見てみると
あるクラスで属性を定義すると、後にそのクラスのサブクラスが作成されるときに名前が衝突するリスクが生じます。これはライブラリにおいては特に重要な問題です。
Active Supportでは、attr_internal_reader、attr_internal_writer、attr_internal_accessorというマクロが定義されています。これらのマクロは、Rubyにビルトインされているattr_*と同様に振る舞いますが、内部のインスタンス変数の名前が衝突しにくいように配慮される点が異なります。
attr_internalマクロはattr_internal_accessorと同義です。
# ライブラリ class ThirdPartyLibrary::Crawler attr_internal :log_level end # クライアントコード class MyCrawler < ThirdPartyLibrary::Crawler attr_accessor :log_level end
サブクラスに定義した名前が衝突してしまうケースを発生させない為の機能になります
ソースコードを読んでみる
1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def attr_internal
を探してみます
2. attr_internal関連の処理がまとまってそうなファイルが見つかったので、みてみます
1. activesupport > lib > active_support > core_ext > module > attr_internal.rb
省略 alias_method :attr_internal, :attr_internal_accessor 省略
20行目でalias_method
を使用し、attr_internal_accessor
に attr_internal
という別名を付け、こちらでも呼び出せるようにしています
alias_methodメソッドは、クラスやモジュールのメソッドに別名を付けます。引数には新しい別名new_name、元のメソッド名original_nameを順に指定します。戻り値はクラスやモジュール自身です。
別名を付けると、通常のメソッド呼び出しのように別名でメソッドを呼び出せるようになります。
class Cat def message() "meow" end alias_method :welcome, :message end cat = Cat.new puts cat.welcome
attr_internal_accessor
の処理を見てみます
省略 # Declares an attribute reader and writer backed by an internally-named instance # variable. def attr_internal_accessor(*attrs) attr_internal_reader(*attrs) attr_internal_writer(*attrs) end 省略
内部でそれぞれ処理を呼んでいるだけのメソッドになります
# frozen_string_literal: true class Module # Declares an attribute reader backed by an internally-named instance variable. def attr_internal_reader(*attrs) attrs.each { |attr_name| attr_internal_define(attr_name, :reader) } end # Declares an attribute writer backed by an internally-named instance variable. def attr_internal_writer(*attrs) attrs.each { |attr_name| attr_internal_define(attr_name, :writer) } end 省略
それぞれの処理を見てみると、受け取った引数(属性名の配列)を eachで1つずつ取り出し attr_internal_define
に渡している処理になります
attr_internal_define
という名前から推測すると内部属性の読み取り、書き取りを定義している処理で引数の :reader
、:writer
で読み取り用、書き込み用の処理を動的に定義しているのではと思われます。
attr_internal_define
を見てみます
省略 private 省略 def attr_internal_define(attr_name, type) internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, "") # use native attr_* methods as they are faster on some Ruby implementations 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 end
attr_internal_ivar_name(attr_name)
の戻り値から @
を取り除いて、internal_name
変数に入れています。
次のsend("attr_#{type}", internal_name)
ではtypeがreader
である場合はインスタンス変数の読み込み用メソッド、 writer
である場合は、書き込み用メソッドを定義しています
次の行ではtypeがwriterである場合は、新しく定義するアクセッサ−をattr_name
変数へ、内部で使用するアクセッサーを internal_name
へ入れています
そして、alias_method
で新しく定義するアクセッサーへと変更しています
そしてメソッドの説明であったメソッド名がサブクラスで衝突するのを避ける為
あるクラスで属性を定義すると、後にそのクラスのサブクラスが作成されるときに名前が衝突するリスクが生じます。これはライブラリにおいては特に重要な問題です。
に、 remove_method
で send("attr_#{type}",
internal_name)
で定義したアクセッサーを削除しています
読んでみて
ライブラリを作る時にアクセッサーを定義した場合には、なるべく使うようにしないとバグを生み出す可能性がかなり高くなりそうだと感じました