Edited at

いつも忘れるRubyの include, prepend, extend の意味。そしてActiveSupport::Concernについても。

まずは、ざっくりシンプルに特徴だけを列挙。


include


  • 見えないsuperclassとして継承階層の上(親クラス側)に挿入される。

  • moduleのインスタンスメソッドがインスタンスメソッドとして使えるようになる


prepend ※2.0以降


  • includeと違い、継承階層の下(派生クラス側)に追加される。

  • include同様、moduleのインスタンスメソッドがインスタンスメソッドとして使えるようになる


extend

モジュールのインスタンスメソッドが、self の特異メソッドとして追加される。

そして、class内でextendするパターンと、インスタンスにextendするパターンがある。


class内でextendする例

M#instance_method が self(ここではclassインスタンス)の特異メソッド(インスタンスメソッド)として取り込まれる。

結果として Cクラスのクラスメソッドとして追加されることになる。

module M

def instance_method; xxx; end
end
class C
extend M


インスタンスにextendする例

この例の場合は、objの特異メソッド(つまり、objのインスタンスメソッド)として取り込まれる。

obj = Object.new

obj.extend M


確認用コードサンプル

module M

def instance_method_a; puts "Called M!"; end
end

# モジュールM を KlassI/KlassP/KlassE にそれぞれ include/prepend/extendしてみた。
# さらに同名のインスタンスメソッドも定義してある。
class KlassI
include M
def instance_method_a; puts "Called I!"; end
end
class KlassP
prepend M
def instance_method_a; puts "Called P!"; end
end
class KlassE
extend M
def instance_method_a; puts "Called E!"; end
end

puts "=== included? ==="
# include/prepend した場合はどちらもMがincludeされていることが分かる。
puts "KlassI: #{KlassI.include?(M)}" # => true
puts "KlassP: #{KlassP.include?(M)}" # => true
puts "KlassE: #{KlassE.include?(M)}" # => false

puts "=== include_modules ==="
puts "KlassI: #{KlassI.included_modules}" # => [M, Kernel]
puts "KlassP: #{KlassP.included_modules}" # => [M, Kernel]
puts "KlassE: #{KlassE.included_modules}" # => [Kernel]

puts "=== ancestors ==="
# ancestorsで見えるクラス継承関係を見てみた。
# この配列の左側からメソッドが探索されるため、同名メソッドが複数クラスで定義されている場合、
# より左側のクラスに定義されたメソッドが優先的に呼び出されることになる。
# includeした場合は M が自クラス(KlassI)の上(親クラス側)に挿入され、
# prependした場合は M が自クラス(KlassP)の下(派生クラス側)に追加されていることが分かる。
puts "KlassI: #{KlassI.ancestors}" # => [KlassI, M, Object, Kernel, BasicObject]
puts "KlassP: #{KlassP.ancestors}" # => [M, KlassP, Object, Kernel, BasicObject]
puts "KlassE: #{KlassE.ancestors}" # => [KlassE, Object, Kernel, BasicObject]

# ★実際に呼び出してみる。ここがincludeとprependで決定的に違うところ。
# include した場合はクラスに定義していたメソッドが呼び出されていて、
# prepend した場合はモジュールに定義していたメソッドが呼び出されていることが分かる。
KlassI.new.instance_method_a # => "Called I!"
KlassP.new.instance_method_a # => "Called M!"

puts "=== singleton_methods ==="
# class内でextendした場合は、クラスの特異メソッド(いわゆるクラスメソッド)として
# 使えるようになっていることが分かる。
puts "KlassI: #{KlassI.singleton_methods}" # => []
puts "KlassP: #{KlassP.singleton_methods}" # => []
puts "KlassE: #{KlassE.singleton_methods}" # => [:instance_method_a]

puts "=== extended to a class ==="
# モジュールで定義されているメソッドも呼び出せるし(前者)、
# もともとクラスで定義していたインスタンスメソッドも呼び出せる(後者)。
KlassE.instance_method_a # => "Called M!"
KlassE.new.instance_method_a # => "Called E!"

puts "=== extend to a instance ==="
obj = Object.new
obj.extend M

# objの特異メソッド(つまり、インスタンスメソッド)として使えるようになっている。
p obj.singleton_methods # => [:instance_method_a]
obj.instance_method_a # => "Called M!"


ActiveSupport::Concern

ActiveSupport::Concern でハッピーなモジュールライフを送る

ActiveSupport::Concern - Railsのソースとか読みはじめた

モジュール側で extend ActiveSupport::Concern しておくと、include + (class内での)extend が一度に出来るようになる。

それだけでなく、複雑な依存関係も上手く隠蔽して、ユーザー(モジュールを使う側)に余計な手間を取らせない。詳細はこちら。[Rails] ActiveSupport::Concern の存在理由


moduleのコード

require 'active_support/concern'

module M
extend ActiveSupport::Concern

# ここに定義したメソッドはinstanceメソッドとして取り込まれる
def m1
end

included do
# attrやscopeはここで。includedで定義したattr,scopeがincludeした側で使えるようになる。
attr_accessor :hoge
scope :not_deleted, -> { where(deleted_at: nil) }
end

class_methods do
# class_methodsに定義したメソッドはclassメソッドとして取り込まれる
# ※このメソッドをこのモジュールM内から呼び出したい場合にはどうすればいい???
def a_class_method
end
end

private
# private instanceメソッドとして取り込まれる
def local_method
end
end



確認用コードサンプル

require 'active_support/concern'

module ConcernM
extend ActiveSupport::Concern

def instance_method_a; puts "Called ConcernM instance_method_a!"; end

class_methods do
def class_method_a; puts "Called ConcernM class_method_a."; end
end
end

# ConcernMをincludeしたクラス
class KlassIC
include ConcernM
def instance_method_a; puts "Called IC!"; end # ConcernMにあるのと同名のインスタンスメソッドを定義
end

# ConcernMをprependしたクラス
class KlassPC
prepend ConcernM
def instance_method_a; puts "Called PC!"; end # ConcernMにあるのと同名のインスタンスメソッドを定義
end

# 比較のために、普通のModuleとそれをincludeしたKlassIも用意しておく
module M
def instance_method_a; puts "Called M!"; end
end
class KlassI
include M
def instance_method_a; puts "Called I!"; end
end

puts "=== included? ==="
puts "KlassI : #{KlassI.include?(M)}" # => true
puts "KlassIC: #{KlassIC.include?(ConcernM)}" # => true
puts "KlassPC: #{KlassPC.include?(ConcernM)}" # => true

puts "=== include_modules ==="
puts "KlassI : #{KlassI.included_modules}" # => [M, Kernel]
puts "KlassIC: #{KlassIC.included_modules}" # => [ConcernM, Kernel]
puts "KlassPC: #{KlassPC.included_modules}" # => [ConcernM, Kernel]

puts "=== ancestors ==="
# KlassPC: ConcernMがちゃんとprependされている
puts "KlassI : #{KlassI.ancestors}" # => [KlassI, M, Object, Kernel, BasicObject]
puts "KlassIC: #{KlassIC.ancestors}" # => [KlassIC, ConcernM, Object, Kernel, BasicObject]
puts "KlassPC: #{KlassPC.ancestors}" # => [ConcernM, KlassPC, Object, Kernel, BasicObject]

KlassIC.new.instance_method_a # => "Called IC!" # 当然KlassIC側に定義したメソッドが呼ばれる
KlassPC.new.instance_method_a # => "Called ConcernM instance_method_a!" # こちらはmodule側に定義したメソッドが呼ばれる

puts "=== singleton_methods ==="
# KlassIC: 普通のモジュールMの場合と違い、class_methodsで定義したメソッドも取り込まれている
# KlassPC: prependした場合は、class_methodで定義したメソッドが取り込まれないようだ。
puts "KlassI : #{KlassI.singleton_methods}" # => []
puts "KlassIC: #{KlassIC.singleton_methods}" # => [:class_method_a]
puts "KlassPC: #{KlassPC.singleton_methods}" # => []

KlassIC.class_method_a # => Called ConcernM class_method_a. # ちゃんと呼び出せる
# KlassPC.class_method_a # NoMethodErrorになる



参考

includeされた時にクラスメソッドとインスタンスメソッドを同時に追加する頻出パターン