Ruby のクラスでモジュールの include, extend, prepend の違い
Ruby のクラスを定義する時, モジュールを mixin として include, extend, prepend すると一体どのような違いがあるのでしょうか.
この記事のサンプルコードは次の環境での動作を確認しております:
- Ruby –
2.5.1
はじめ
Ruby は多重継承という概念がありません.
そして, その概念と同等の機能の役割を果たす mixin と呼ばれるものがあります.
それはモジュールをクラスで include, extend, prepend してそのモジュールに定義されているメソッドをそのクラスでも使えるようにするというものです.
クラスのメソッドはクラスメソッドとインスタンスメソッドの 2 種類がありますが, include, prepend されたモジュールのメソッドはインスタンスメソッドになり, extend されたモジュールのメソッドはクラスメソッドになります.
include と prepend は共にインスタンスメソッドになるのですが, 前者の場合, include を定義したクラスが include に指定されたモジュールをクラスの継承のように取り込み, 後者の場合, prepend を定義したクラスが prepend に指定されたモジュールに継承されるかのようになります.
なので include と prepend では取り込まれるモジュールのヒエラルキがクラスと入れ替われます.
include, extend, prepend それぞれの違いを詳しく見ていきたいと思います.
include
次のコードがあるとします:
module M
def hello
'Hello World'
end
end
class Klass
include M
end
モジュール M に hello メソッドが定義されています. そして M をクラス Klass で include しています.
すると Klass のインスタンスメソッドとして, M の hello メソッドを呼び出せるようになります:
Klass.new.hello #=> "Hello World"
また include した M によってもたらされた Klass の hello インスタンスメソッドは, Klass に同名のインスタンスメソッドを定義してオーバーライドが可能です:
Klass.class_eval do
def hello
super.upcase
end
end
Klass.new.hello #=> "HELLO WORLD"
あと Module#ancestors で Klass に include もしくは prepend されているモジュールを見てみると, Klass の前に M がいることを確認できます:
Klass.ancestors #=> [Klass, M, Object, Kernel, BasicObject]
もし次の新たなモジュール M2 を作り:
module M2
def hello
super + '!'
end
end
Klass に include すると:
Klass.include M2
Klass の hello インスタンスメソッドの返り値は次のようになります:
Klass.new.hello #=> "HELLO WORLD!"
Module#ancestors で Klass を見てみると, 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 の時の Klass の include が extend に変わっているだけです.
するとクラス Klass で extend したモジュール M の hello メソッドが, Klass のクラスメソッドとして使えるようになります:
Klass.hello #=> "Hello World"
include の時と同じく, hello クラスメソッドのオーバーライドも可能です:
Klass.class_eval do
def self.hello
super.upcase
end
end
Klass.hello #=> "HELLO WORLD"
ただ Module#ancestors で Klass を見てみても, 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
モジュール M の hello メソッドで super キーワードを使っていますが, これはこの M がクラス Klass で prepend され, Klass に hello インスタンスメソッドが定義されている為, その super を使うことができます.
つまりは Klass の hello を M の hello がオーバーライドしていることになります:
Klass.new.hello #=> "HELLO WORLD"
このようにクラスの既存のインスタンスメソッドに何か機能を加えたいという場合に prepend を使うことができます.
Module#ancestors で Klass を見てみると, Klass と後に M がいることを確認できます:
Klass.ancestors #=> [M, Klass, Object, Kernel, BasicObject]
include の時の Klass.ancestors の結果と比べてみるとわかるように, M が Klass の後に来ています.
まとめ
まとめますと, クラスでモジュールをそれぞれ include, extend, prepend した時の違いは次のようになります:
include– モジュールのメソッドがクラスのインスタンスメソッドになりますが,Module#ancestorsの順序はモジュールの後にクラスが来ます.extend– モジュールのメソッドがクラスのクラスメソッドになります.prepend– モジュールのメソッドがクラスのインスタンスメソッドになりますが,Module#ancestorsの順序はクラスの後にモジュールが来ます.
参考資料
関連記事
Ruby の正規表現を備忘録としてまとめてみた2018.08.30
Ruby の関数プログラミングでオイラー積を計算してみた2018.07.02
Ruby のクラスやメソッドのドキュメントを見れる ri コマンドが便利な件2018.09.08
Bundler をシングルファイル Ruby スクリプトで使う方法2018.04.23
Git で変更されたファイルを部分的にステージする方法2018.11.01