include
と extend
の違いってなんだ?
内部では何が起きているのか..?
ActiveSupport::Concern
を使うにも included
使うけど何をしているのか
ActiveSupport::Concern
を使う際に学んだことをメモします。
extendについて
-
extend
したクラスのシングルトンクラス(特異クラス)のインスタンスメソッドとして追加できる -
extend
したモジュールのメソッドはクラスメソッドのように呼び出せる
順を追うと...
シングルトンクラスとは...
あるオブジェクト専用のクラスであり、そのオブジェクトだけに属する
こちらが参考になります
ちなみにobject.singleton_classで確認することができる
class Bar; end
p Bar.singleton_class # => #<Class:Bar>
bar = Bar.new
p bar.singleton_class # => #<Class:#<Bar:0x0000000102846720>>
Barクラス
は Classクラス
のオブジェクトなので、
Classクラス
のシングルトンクラスが生成されます。
また、「そのオブジェクトのみに属する」という特徴があるため、オブジェクトを別々で生成した場合も、それぞれでシングルトンクラスが生成されます。(ややこしい...)
bar = Bar.new
bar_1 = Bar.new
p bar.singleton_class # => #<Class:#<Bar:0x00000001007a9670>>
p bar_1.singleton_class # => #<Class:#<Bar:0x00000001007a95d0>>
特異メソッドとクラスメソッドの関連性
クラスメソッドは特異メソッドの一種と見なすことができます。
というのも
クラスメソッドは特異クラスのインスタンスメソッドになるからです。
実際に確認してみましょう。
class Bar
# クラスメソッドを定義
def self.good
p 'いいね!!'
end
end
p Bar.singleton_class.instance_methods # => [:good,...]
上記のように Bar
のクラスメソッドは、
シングルトンクラスのインスタンスメソッドに定義されていそうです。
シングルトンメソッドを追加できる
define_singleton_methodでシングルトンメソッドを後で追加することができます。
class Bar
def self.good
p 'いいね!!'
end
end
Bar.define_singleton_method :nice do
p '素敵!'
end
p Bar.singleton_class.instance_methods # => [:nice, :good,...]
p Bar.singleton_methods # => [:nice, :good]
さて、extendについて
extend
したモジュールのメソッドはクラスメソッドのように呼び出せる
上記のように説明できる理由として
クラスメソッドは
シングルトンクラスのインスタンスメソッド(特異メソッド)であり、
extendは
特異メソッドに新しくメソッドを追加できる
からです。
module Foo
def hello(name)
p "こんにちは、#{name}さん"
end
end
class Bar
extend Foo
end
# シングルトンクラスに hello メソッドが追加されている
p Bar.singleton_methods # => [:hello]
# クラスメソッドのように hello メソッドを呼び出す
Bar.hello('太郎') # => こんにちは、太郎さん
# インスタンスメソッドとして呼び出そうとすると失敗する
# Bar クラスのインスタンスである bar のシングルトンメソッドには hello が定義されていないから...!
bar = Bar.new
p bar.hello # => NoMethodError: undefined method `hello' for #<Bar:0x...
include について
include
とは、指定されたモジュールの定義 (メソッド、定数) を引き継ぐことです。
モジュールによる機能追加は、そのクラスとスーパークラスとの継承関係の間にそのモジュールが挿入されることで実現されます。
そのため多重継承の代わりに用いられており、 Mix-in
とも呼ばれます。
module Foo
def hello(name)
puts "こんにちは、#{name}さん"
end
end
class Hoge; end
class Bar < Hoge
include Foo
end
# includeされるクラスとそのスーパークラスとの間に差し込まれる
p Bar.ancestors # => [Bar, Foo, Hoge, Object, Kernel, BasicObject]
# includeしたモジュールのインスタンスメソッドを引き継げる
Bar.new.hello('太郎') # => こんにちは、太郎さん
さて、ActiveSupport::Concernですよ...
ActiveSupport::Concernのソースを参照しながら見ていきたいと思います。
普通にextend, includeではダメなのか?
こちらの記事に分かりやすく書かれていました。
[Rails] ActiveSupport::Concern の存在理由
Rubyの mix-in は通常以下のようなルールで記述します
- 切り出した機能を module として作成
- 共通メソッドを module 内に記述
- クラスメソッドや組み込み先クラスの内部処理を module に入れたい場合は特殊なメソッドを使う必要がある
ActiveSupport::Concern はこの3番目にある 特殊なメソッド の記述を簡単にしてくれます。
簡単にいうと、include する側のクラスのコンテキストで実行したい特殊な処理を、スッキリいい感じに書けるようになる。
これに関してはコードを見た方が早いです。
ActiveSupport::Concern なし
-
base
はこのモジュールが取り込まれる先のクラスになる。こちらを参照 -
extend
は言わずもがな。modele内で定義したクラスメソッドを、include
先のシングルトンメソッドとして追加している。 -
class_eval
についてはこちらの記事を参照されたい。要するにメタプログラミング的に、base
のインスタンスメソッドにメソッドを追加することでbaseクラスのコンテキストでメソッドを実行できるようになる。 -
self.included(base)
については、module
にincluded メソッド
を定義しておくと、module
がinclude
された時に実行されるようになる。こちらを参照
module M
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
end
module ClassMethods
...
end
end
ちなみに、
concern
で追加したインスタンスメソッドと、追加先のクラスのインスタンスメソッドの命名がバッティングした場合は、追加先のクラスのインスタンスメソッドが優先的に実行される。
module M
def self.included(base)
base.class_eval do
def example_method
p "追加されたメソッドです"
end
end
end
end
class MyClass
include M
def example_method
p '元々あったメソッドです'
end
end
MyClass.new.example_method # => '元々あったメソッドです'
というのもinclude
されたmodule
は継承関係的に上位に来るので、命名が被った場合は小クラスのメソッドが優先的に実行されるからです。
p MyClass.ancestors #=> [MyClass, M, Object, Kernel, BasicObject]
ActiveSupport::Concern あり
-
include
先のコンテキストで実行したいものはinclude do; end
内に記述 - クラスメソッド的に使用したいものは
class_method
に記述
module M
extend ActiveSupport::Concern
included do
scope :disabled, -> { where(disabled: true) }
end
class_methods do
...
end
end
シンプルな書き方でいい感じに処理できるようになりました。🎉
他にも 依存関係の解消までやってくれる などの利点があるのだが、
ここでは触れないでおく。
内部ではどんな処理をしているのか
module Concern
class MultipleIncludedBlocks < StandardError #:nodoc:
def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end
class MultiplePrependBlocks < StandardError #:nodoc:
def initialize
super "Cannot define multiple 'prepended' blocks for a Concern"
end
end
def self.extended(base) #:nodoc:
base.instance_variable_set(:@_dependencies, [])
end
def append_features(base) #:nodoc:
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
def prepend_features(base) #:nodoc:
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies).unshift self
false
else
return false if base < self
@_dependencies.each { |dep| base.prepend(dep) }
super
base.singleton_class.prepend const_get(:ClassMethods) if const_defined?(:ClassMethods)
base.class_eval(&@_prepended_block) if instance_variable_defined?(:@_prepended_block)
end
end
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
end
def prepended(base = nil, &block)
if base.nil?
if instance_variable_defined?(:@_prepended_block)
if @_prepended_block.source_location != block.source_location
raise MultiplePrependBlocks
end
else
@_prepended_block = block
end
else
super
end
end
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
end
end
はい、意味がわかりませんね😭
一つずつ噛み砕いていく
self.extended(base)
モジュールをextend
した時に呼ばれ、extended
を呼び出してインスタンス変数である@_dependencies
をセット。
def self.extended(base) #:nodoc:
base.instance_variable_set(:@_dependencies, [])
end
実際に使われる、included
, class_methods
について見てみる
included
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
end
base
の初期値は nil
が確定
続いて if instance_variable_defined?(:@_included_block)
では、初回実行時はfalse
が確定なので @_included_block = block
が実行される。
class_methods
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
が存在するかを確認する。
(falseオプションで継承関係を無視してそのクラス内のみで確認する)
もし、見つかればそれを取得して、なければ module
として定義する。
その後、module_eval によって ClassMethods
のインスタンスメソッドとしてブロック引数である &class_methods_module_definition
を追加している。
つまり
module Sample
extend ActiveSupport::Concern
class_methods do
def class_method
p 'クラスメソッドです'
end
end
end
これが
↓
こうなっているのと同じ状態
module Sample
module ClassMethods
def class_method
'クラスメソッドです'
end
end
end
ClassMethods
モジュールを作成し、その内部には引数で受けたメソッドがインスタンスメソッドとして定義される。
閑話休題
現状
included
: @_dependencies
をセットする
class_methods
: ClassMethods
モジュールを作成し、ブロックで指定したメソッドを持たせる
この段階では include
されるクラスにクラスメソッドを追加したり、include
される側のコンテキストで処理を実行することはできない。
そこで、重要となってくるのが append_features
メソッドらしい。
append_features
このメソッドは Module#include の実体であり、...
と説明がある通り、included
された時に呼ばれることがわかります
試してみる🧐
module M
def self.append_features(_base)
p 'includeされた'
end
end
class MyClass; include M; end
MyClass.new => # 'includeされた'
確かにinclude
される時に実行される。
つまり、 ActiveSupport::Concern
を extend
したモジュールを include
すると、内部で実行されるはずの append_features メソッド
を独自のメソッドでオーバーライドしているようだ。
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
-
base
とはinclude
される側のクラスになる。 -
if base.instance_variable_defined?(:@_dependencies)
の条件分岐では、
include
される側のクラスがconcern
でない場合、@_dependencies
がないためfalse
になる。 -
super
によって既存のappend_features
が継承されモジュールがinclude
される。
ちなみにsuper
がないと、include
できないようだ(こちらを参照) -
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
によって
ClassMethods
モジュールをextend
することでClassMethods
に定義されたメソッドをbase
クラスのクラスメソッドとして追加することができる。 -
base.class_eval(&@_included_block)
によって
class_eval
によりbase
クラスのコンテキストで@_included_block
を実行できるようになる。
以上、included
, class_methods
, append_features
によってActiveSupport::Concern
の大枠の仕組みが実現できているようだ。
難しい(;´д`)
余談
prepended は Module#prepend
されたときに実行されるので、今回はスルーします。
prepend_features は Module#prepend
から呼び出されるメソッドで、 prepend
の処理の実体であるため、こちらも同様にスルーします。
最後に
ここまで見ていただきありがとうございます。m(._.)m
extend
や include
の内部挙動について知れたのは個人的に良かったです。
ActiveSupport::Concern
が開発された背景である 「モジュールの依存関係を解消する」 については今回ノータッチだったので、別の機会に勉強できたらと思います。