###はじめに
Railsのソースコードを読んでいると頻繁に出くわすActiveSupport::Concernモジュールについて、その使い方とConcernモジュールで行っていることをまとめました。
対象のバージョンは、rails5.2.1です。
###記事の目的
railsのソースコードを読むために学習した内容を整理してみました。
最近railsの勉強を始めた初心者なのですが、私と同じような初心者の方の学習の助けになったら嬉しく思います。
また、一次情報にあたるなどしてなるべく間違いの無いようにしたつもりなのですが、もしも記事の内容に間違いがありましたら、大変お手数ですがご教示頂けますと嬉しいです。
###ActiveSupport::Concernモジュールとは
RubyonRailsのコンポーネントであり、Ruby言語の拡張などを行っているActiveSupportの一部。
モジュールをインクルードした際に、インスタンスメソッドと一緒にクラスメソッドもインクルーダーに追加する機能をカプセル化したもの。1
あらかじめモジュールにActiveSupport::Concernをエクステンドしておくことによって、クラスメソッドの追加機能を簡単に使えるようになる。
Railsガイド ActiveSupport
###ActiveSupport::Concernモジュールの使い方
require 'active_support'
module MyConcern
#モジュールの中でActiveSupport::Concernモジュールをエクステンドする
extend ActiveSupport::Concern
def my_instance_method
"my_instance_method()"
end
#ClassMethodsモジュールの中で、インクルーダーに追加するクラスメソッドを定義する
module ClassMethods
def my_class_method
"my_class_method()"
end
end
end
class MyClass
#ActiveSupport::Concernをエクステンドしたモジュールをインクルードする
include MyConcern
end
obj = MyClass.new
obj.my_instance_method #=> "my_instance_method()"
#MyClassに対してMyConcern::ClassMethodsで定義したメソッドを呼び出す
MyClass.my_class_method #=> "my_class_method()" #インクルーダーにクラスメソッドとして追加できている!
このように、ActiveSupport::Concern
をエクステンドしたモジュールの中では、ClassMethods
モジュールのスコープ内で定義されたメソッドをインクルーダーのクラスメソッドとして追加することができるようになる。
###ActiveSupport::Concernモジュールの仕組み
####ソースコードを読んでみる
module ActiveSupport
module Concern
class MultipleIncludedBlocks < StandardError #:nodoc:
def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end
def self.extended(base) #:nodoc:
base.instance_variable_set(:@_dependencies, [])
end
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
false
else
return false if base < self
@_dependencies.each { |dep| base.include(dep) }
super
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
#...以下省略
end
end
ActiveSupport::Concern
モジュールのソースコードを理解するためには、extended
メソッドとappend_features
メソッドについて詳しい定義を知る必要がある。
#####2つのメソッドについての説明
・extended
メソッド
モジュールがクラス(またはモジュール)にエクステンドされた時に呼び出されるフックメソッドである。
module A
def self.extended(mod)
puts "#{self} extended in #{mod}"
end
end
module Enumerable
extend A
end
# => prints "A extended in Enumerable"
引用 : https://ruby-doc.org/core-2.5.1/Module.html#method-i-extended
・append_features
メソッド
Rubyで元々用意されているメソッド(コアメソッド)であり、モジュールをインクルードした時にRubyが呼び出す。
インクルードされたモジュールがインクルーダーの継承チェーンに含まれているかどうかを確認し、含まれていなければ継承チェーンにモジュールを追加する機能を持っている(つまり、インクルード機能の本体を受け持っている)。
そのため、append_featuresをオーバーライドするとモジュールが一切インクルードされなくなる
尚、プライベートメソッドのため直接呼び出すことはできない。
参考:
https://github.com/ruby/ruby/blob/trunk/eval.c#L1204-L1221
https://ruby-doc.org/core-2.5.1/Module.html#method-i-append_features
https://ruby-doc.org/core-2.5.1/Module.html#method-i-include
以上に示した2つのメソッドの定義を踏まえ、実際にActiveSupport::Concernを使用する際の手続きに沿ってモジュールのソースコードを読んでみる。
また、メタプログラミングRubyに習って、ActiveSupport::Concernをエクステンドしているモジュールを便宜的に小文字でconcernと表記する。
#####ActiveSupport::Concernをモジュールへエクステンドする
まず、ActiveSupport::Concernモジュールをモジュールへエクステンドする。
その際に、ActiveSupport::Concernモジュールは、フックメソッドであるextended
を呼び出し、エクステンダー(base)にクラスインスタンス変数@_dependencies
を定義する。(@_dependencies
には、最初は空の配列が入っている。)
ここまでで、全てのconcernはクラスインスタンス変数@_dependencies
と、ActiveSupport::Concernにオーバーライドされたappend_features
メソッドを持つことが分かる。
#####concernをモジュールやクラスにエクステンドする
concernをインクルードする際Rubyがappend_features
メソッドを呼び出す。
ここではオーバーライドされたappend_features
メソッドが呼び出され、インクルーダー(base)がconcernかどうかを確認している。
######インクルーダーがconcernの場合
もしもインクルーダーが@_dependencies
を持っているならば、そのインクルーダーはconcernであるとわかる。
すでにAtiveSupport::Concernをエクステンドしているモジュールに、同じくActiveSupport::Concernをエクステンドしているモジュールをインクルードすると、クラスメソッドを読み込むべきクラスがずれてしまうため、この場合はインクルーダーの継承チェーンに自身を追加する代わりに、@_dependencies
に自身を追加し、依存管理のリストとして保存しておく。
######インクルーダーがconcernでは無い場合
すでに他のconcernがインクルーダーにインクルードされていることなどによって、インクルーダーの継承チェーンに自身が追加されていないか確認している。(base < self)
すでに自身が継承チェーンに追加されていれば、二重にインクルードすることを避けるためにfalseを返す。
継承チェーンに追加されていなければ、インクルーダーに依存関係を再帰的にインクルードする。(全ての依存関係をインクルーダーに一気に流し込む)
@_dependencies
に入っている全てのconcernをインクルーダーの継承チェーンに追加した後、super
で素のappend_features
メソッドを呼び出して自分自身を継承チェーンに追加し、インクルーダーにClassMethods
モジュールをエクステンドさせる。
以上のように、ActiveSupport::Concernは依存関係をうまく管理しながら、モジュールにクラスメソッドの読み込みを行わせている。
###まとめ
ActiveSupport::Concernのしくみという題名なのに全然うまく説明できてない感がすごいので、特に依存関係を再帰的にインクルードするところなどをうまく説明できるようにしたいです。メタプログラミングRubyはすごい。
###参考書籍
-
インクルーダー ... インクルード先のモジュール(クラス)という意味で使用しています ↩