Ruby

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

詳しくはこちら。
includeされた時にクラスメソッドとインスタンスメソッドを同時に追加する頻出パターン

以下、ざっくりシンプルに特徴だけを列挙。

include

  • 見えないsuperclassとして継承階層の上(親クラス側)に挿入される。
  • moduleのインスタンスメソッドがインスタンスメソッドとして使えるようになる

prepend ※2.0以降

  • includeと違い、継承階層の下(派生クラス側)に追加される。
  • include同様、moduleのインスタンスメソッドがインスタンスメソッドとして使えるようになる

extend

モジュールのインスタンスメソッドが、self の特異メソッドとして追加される。
そして、class内でextendするパターンと、インスタンスにextendするパターンがある。

例1)class内でextend

M#instance_method が self(ここではclassインスタンス)の特異メソッド(インスタンスメソッド)として取り込まれる。
結果として Cクラスのクラスメソッドとして追加されることになる。

module M
  def instance_method; xxx; end
end
class C
  extend M

例2)インスタンスに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になる