Ruby のクラスでモジュールの include, extend, prepend の違い


Ruby のクラスを定義する時, モジュールを mixin として include, extend, prepend すると一体どのような違いがあるのでしょうか.

この記事のサンプルコードは次の環境での動作を確認しております:

  • Ruby – 2.5.1

はじめ

Ruby は多重継承という概念がありません.

そして, その概念と同等の機能の役割を果たす mixin と呼ばれるものがあります.

それはモジュールをクラスで include, extend, prepend してそのモジュールに定義されているメソッドをそのクラスでも使えるようにするというものです.

クラスのメソッドはクラスメソッドとインスタンスメソッドの 2 種類がありますが, include, prepend されたモジュールのメソッドはインスタンスメソッドになり, extend されたモジュールのメソッドはクラスメソッドになります.

includeprepend は共にインスタンスメソッドになるのですが, 前者の場合, include を定義したクラスが include に指定されたモジュールをクラスの継承のように取り込み, 後者の場合, prepend を定義したクラスが prepend に指定されたモジュールに継承されるかのようになります.

なので includeprepend では取り込まれるモジュールのヒエラルキがクラスと入れ替われます.

include, extend, prepend それぞれの違いを詳しく見ていきたいと思います.

include

次のコードがあるとします:

module M
  def hello
    'Hello World'
  end
end

class Klass
  include M
end

モジュール Mhello メソッドが定義されています. そして M をクラス Klassinclude しています.

すると Klass のインスタンスメソッドとして, Mhello メソッドを呼び出せるようになります:

Klass.new.hello #=> "Hello World"

また include した M によってもたらされた Klasshello インスタンスメソッドは, Klass に同名のインスタンスメソッドを定義してオーバーライドが可能です:

Klass.class_eval do
  def hello
    super.upcase
  end
end

Klass.new.hello #=> "HELLO WORLD"

あと Module#ancestorsKlassinclude もしくは prepend されているモジュールを見てみると, Klass の前に M がいることを確認できます:

Klass.ancestors #=> [Klass, M, Object, Kernel, BasicObject]

もし次の新たなモジュール M2 を作り:

module M2
  def hello
    super + '!'
  end
end

Klassinclude すると:

Klass.include M2

Klasshello インスタンスメソッドの返り値は次のようになります:

Klass.new.hello #=> "HELLO WORLD!"

Module#ancestorsKlass を見てみると, M の後に M2 が挿入されていることを確認できます:

Klass.ancestors #=> [Klass, M2, M, Object, Kernel, BasicObject]

なので Hello World, Hello World!, HELLO WORLD!hello メソッドがオーバーライドされて来たことを確認できます.

extend

次のコードがあるとします:

module M
  def hello
    'Hello World'
  end
end

class Klass
  extend M
end

include の時の Klassincludeextend に変わっているだけです.

するとクラス Klassextend したモジュール Mhello メソッドが, Klass のクラスメソッドとして使えるようになります:

Klass.hello #=> "Hello World"

include の時と同じく, hello クラスメソッドのオーバーライドも可能です:

Klass.class_eval do
  def self.hello
    super.upcase
  end
end

Klass.hello #=> "HELLO WORLD"

ただ Module#ancestorsKlass を見てみても, M が何処にも挿入されていないことを確認できます:

Klass.ancestors #=> [Klass, Object, Kernel, BasicObject]

prepend

次のコードがあるとします:

module M
  def hello
    super.upcase
  end
end

class Klass
  prepend M

  def hello
    'Hello World'
  end
end

モジュール Mhello メソッドで super キーワードを使っていますが, これはこの M がクラス Klassprepend され, Klasshello インスタンスメソッドが定義されている為, その super を使うことができます.

つまりは KlasshelloMhello がオーバーライドしていることになります:

Klass.new.hello #=> "HELLO WORLD"

このようにクラスの既存のインスタンスメソッドに何か機能を加えたいという場合に prepend を使うことができます.

Module#ancestorsKlass を見てみると, Klass と後に M がいることを確認できます:

Klass.ancestors #=> [M, Klass, Object, Kernel, BasicObject]

include の時の Klass.ancestors の結果と比べてみるとわかるように, MKlass の後に来ています.

まとめ

まとめますと, クラスでモジュールをそれぞれ include, extend, prepend した時の違いは次のようになります:

  • include – モジュールのメソッドがクラスのインスタンスメソッドになりますが, Module#ancestors の順序はモジュールの後にクラスが来ます.
  • extend – モジュールのメソッドがクラスのクラスメソッドになります.
  • prepend – モジュールのメソッドがクラスのインスタンスメソッドになりますが, Module#ancestors の順序はクラスの後にモジュールが来ます.

参考資料