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