Railsのソースコード読んでみる | Active Support attr_internal編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っている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

引用:RAILS GUIDES#内部属性

サブクラスに定義した名前が衝突してしまうケースを発生させない為の機能になります

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def attr_internal を探してみます

f:id:sktktk1230:20180509155702p:plain

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_accessorattr_internal という別名を付け、こちらでも呼び出せるようにしています

alias_methodメソッドは、クラスやモジュールのメソッドに別名を付けます。引数には新しい別名new_name、元のメソッド名original_nameを順に指定します。戻り値はクラスやモジュール自身です。

別名を付けると、通常のメソッド呼び出しのように別名でメソッドを呼び出せるようになります。

class Cat
  def message() "meow" end

  alias_method :welcome, :message
end

cat = Cat.new
puts cat.welcome

引用:Rubyリファレンス#alias_method

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_methodsend("attr_#{type}", internal_name) で定義したアクセッサーを削除しています

読んでみて

ライブラリを作る時にアクセッサーを定義した場合には、なるべく使うようにしないとバグを生み出す可能性がかなり高くなりそうだと感じました