ActiveSupport::Concernが裏でやっていること
ActiveSupport::Concern を extend したモジュールは以下の機能が使えるようになる。
class_methods do .. endブロックに定義したメソッドをクラスメソッドとして、include する側のクラスに取り込むincluded do .. endブロックに定義した処理を include する側のクラスのコンテキストで実行する
これらの機能を実現するために、Rails内部でどのようなことをしているかを調べた。
Railsのバージョン
サンプルコード
まずはサンプルコードでそれぞれの機能を使ったときの動作を確認しよう。
class_methods を使ったサンプルコード
app/models/concerns/sample.rb モジュールを作成し、class_methods ブロックの中にメソッドを1つ定義する。
# app/models/concerns/sample.rb
module Sample
extend ActiveSupport::Concern
class_methods do
def sample_class_method
'sample_class_method'
end
end
endこれをモデルのUserクラスの中で include する。
# app/models/user.rb
class User < ApplicationRecord
include Sample
endすると、次のようにクラスメソッドが使えるようになる。
[1] pry(main)> User.sample_class_method
=> "sample_class_method"included を使ったサンプルコード
先ほど作成した app/models/concerns/sample.rb モジュールに対し、今度は included ブロックに validates の処理を書く。
# app/models/concerns/sample.rb
module Sample
extend ActiveSupport::Concern
included do
validates :name, presence: true
end
end同じようにUserクラスの中で include する。
# app/models/user.rb
class User < ApplicationRecord
include Sample
end次のように、Userクラスに validates が効いた状態になっている
[1] pry(main)> user = User.new(name: nil)
=> #<User:0x00007fbda44d76a0 id: nil, name: nil, created_at: nil, updated_at: nil>
[2] pry(main)> user.valid?
=> false
[3] pry(main)> user.errors[:name]
=> ["can't be blank"]class_methods の実装
それでは、#class_methods の実装を見てみよう。
# https://github.com/rails/rails/blob/5-2-stable/activesupport/lib/active_support/concern.rb#L140-L146
def class_methods(&class_methods_module_definition)
mod = const_defined?(:ClassMethods, false) ?
const_get(:ClassMethods) :
const_set(:ClassMethods, Module.new)
mod.module_eval(&class_methods_module_definition)
end最初に const_defined?(:ClassMethods, false) で、ClassMethods という定数(モジュール)が存在するかを確認し、あればそれを取得、なければモジュールとして新しく定義している。
その後、#module_eval をもって ClassMethods のコンテキストで引数の &class_methods_module_definition ブロックを実行している。
これが実行されると、先のサンプルコードであれば次の状態になったことと同義となる。
module Sample
module ClassMethods
def sample_class_method
'sample_class_method'
end
end
endincluded の実装
続いて #included の実装を見てみよう。
# https://github.com/rails/rails/blob/5-2-stable/activesupport/lib/active_support/concern.rb#L126-L138
def included(base = nil, &block)
if base.nil?
if instance_variable_defined?(:@_included_block)
if @_included_block.source_location != block.source_location
raise MultipleIncludedBlocks
end
else
@_included_block = block
end
else
super
end
endincluded do .. end のようにブロック付きで実行される想定なので、引数 base は nil となり最初の条件分岐は真となる。
続いて if instance_variable_defined?(:@_included_block) の条件分岐は最初の実行の場合は偽となるので @_included_block = block の部分だけが実行されることになる。
#append_features
さて、class_methods と included の実装を見たところだが、それぞれ次のことをやっているだけであった
class_methodsはブロックの中の定義をClassMethodsモジュールに展開するincludedはブロックの中の定義を@_included_blockインスタンス変数に代入する
これだけでは include した側のクラスにクラスメソッドを定義したり、include する側のコンテキストでコードを実行することはできない。
ではどうしているか?この謎の答えは ActiveSupport::Concern に定義された #append_features というメソッドにある。
# https://github.com/rails/rails/blob/5-2-stable/activesupport/lib/active_support/concern.rb#L113-L124
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そもそも #append_features は Ruby の Moduleクラスにあるメソッドで、include の実体であるとドキュメントに書かれている。
モジュール(あるいはクラス)に self の機能を追加します。 このメソッドは Module#include の実体であり、...(略)
※ https://docs.ruby-lang.org/ja/latest/method/Module/i/append_features.html
つまり、モジュールを include したとき、内部では #append_features メソッドが実行されるようになっていて、ActiveSupport::Concern はこの #append_features をオーバーライドしているのだ。
このオーバライドした #append_features メソッドの最後の2行に注目してみよう。
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)base というのは include した側のクラスなので、サンプルコードで言えば Userクラスにあたる。
base.extend const_get(:ClassMethods)- Userクラスに対して
ClassMethodsモジュールをextendする - よって、
ClassMethodsに定義されたメソッドが、Userクラスにクラスメソッドとして定義される
- Userクラスに対して
base.class_eval(&@_included_block)class_evalにより Userクラスのコンテキストで@_included_blockを実行する- サンプルコードで言えば、
validates: :name, presence: trueが実行されることになる
このようにして、2つの機能を実現していた。
コードを読むとき、Concernモジュール側の視点とConcernモジュールを include する側の視点とを切り替えなければならず、少し混乱した...。

h3pei
フリーランスのソフトウェアエンジニア。Ruby / Rails アプリケーションの開発が得意領域。設計・実装・運用まで含めてプロダクト開発が好きです。
Questalという目標達成コミュニティサービスを開発しました。仲間と一緒に目標達成に取り組みたい方はぜひご利用ください。