はじめに
クラスにモジュールを取り込む方法として、include
, prepend
, extend
の3つがあります。
この記事では3つの方法の違いと使い分けについてまとめていきたいと思います。
なお、この記事におけるバージョンはRuby 3.3です。
そもそもモジュールとは?
モジュールは関連するメソッドや定数をグループ化したものです。
クラスと似ていますが、以下の2点で異なります。
- モジュールはインスタンス化できない
- モジュールは継承できない
複数のクラスに共通の機能を提供したり、名前空間を分割してメソッド名の衝突を避けたりしたい場合に用いられます。
include
の使い方と特徴
include
はモジュールのメソッドをクラスへ取り込むために使われます。
メソッドの探索順序
include
を使うとモジュールがメソッドの探索順序に組み込まれます。
その結果、次の順序でメソッドが探索されます。
- クラス自身に定義されているメソッド
-
include
されたモジュールのメソッド - スーパークラスのメソッド
メソッドの探索順序において、include
で取り込んだモジュールはクラスの真上に挿入されるイメージとなります。
クラスとスーパークラスの間に挿入された状態です。
具体的なコードで確認していきます。
include
の具体例
以下の例ではGreet
モジュールがChild
クラスに取り込まれています。
そのため、Parent
クラスのhello
メソッドではなく、Greet
モジュールのhello
メソッドが呼び出されているのです。
module Greet
def hello
"Hello from Greet module"
end
end
class Parent
def hello
"Hello from Parent class"
end
end
class Child < Parent
include Greet
end
child = Child.new
child.hello
#=> Hello from Greet module
ancestors
メソッドで探索順序を確認してみます。
Child.ancestors # Greet モジュールがメソッドの探索順序に追加されている
#=> [Child, Greet, Parent, Object, PP::ObjectMixin, Kernel, BasicObject]
たしかにChild
クラスとParent
クラスの間にGreet
モジュールが探索されることを確認できました。
また1つのクラスに複数のモジュールをinclude
した場合の挙動も確認します。
次の例は上記に加えてさらにHoge
モジュールもinclude
する例です。
module Greet
def hello
"Hello from Greet module"
end
end
module Hoge
def hello
"Hello from Hoge module"
end
end
class Parent
def hello
"Hello from Parent class"
end
end
class Child < Parent
include Greet
include Hoge
end
Child.ancestors
#=> [Child, Hoge, Greet, Parent, Object, PP::ObjectMixin, Kernel, BasicObject]
Hoge
モジュールがChild
クラスとGreet
モジュールの間に挿入されています。
Hoge
モジュールがChild
クラスの真上に挿入されることで、元々クラスの真上に存在していたGreet
モジュールは押し上げられてしまうのです。
このように、include
で取り込まれたモジュールはクラスの真上に配置されます。
include
したモジュールは親クラスよりも先に探索される
モジュールAとBをinclude
したら「クラス → B → A → 親クラス」の順で探索する
prepend
の使い方と特徴
prepend
もinclude
と同じく、モジュールをクラスへ取り込むために使います。
メソッドの探索順序
prepend
を使った時のメソッド探索の順序は次のようになります。
-
prepend
されたモジュールのメソッド - クラス自身に定義されているメソッド
- スーパークラスのメソッド
探索順序において、include
はクラスの真上にモジュールを挿入するのに対し、prepend
はクラスの下にモジュールを挿入します。
つまり、クラス自身よりも優先的にモジュールが探索されます。
そのためクラスに定義されているメソッドの上書きが可能です。
prepend
の具体例
include
で使用したサンプルコードを少し書き換えます。
Child
クラスへGreet
モジュールをprepend
で取り込み、またChild
クラス自身にもhello
メソッドを定義しました。
module Greet
def hello
"Hello from Greet module"
end
end
class Parent
def hello
"Hello from Parent class"
end
end
class Child < Parent
prepend Greet
def hello
"Hello from Child class"
end
end
child = Child.new
child.hello
#=> "Hello from Greet module"
Child
クラス自身に定義されているhello
メソッドではなく、Greet
モジュールに定義されたhello
メソッドが実行されました。
こちらもancestors
メソッドで探索順序を確認してみます。
Child.ancestors # Greet モジュールが Child クラスの前に探索されている
#=> [Greet, Child, Parent, Object, JSON::Ext::Generator::GeneratorMethods::Object, PP::ObjectMixin, Kernel, BasicObject]
今度はChild
クラスの前にGreet
モジュールが挿入されていますね。
おなじ流れで複数のモジュールをprepend
した場合の挙動も確認します。
module Greet
def hello
"Hello from Greet module"
end
end
module Hoge
def hello
"Hello from Hoge module"
end
end
class Parent
def hello
"Hello from Parent class"
end
end
class Child < Parent
prepend Greet
prepend Hoge
def hello
"Hello from Child class"
end
end
Child.ancestors
#=> [Hoge, Greet, Child, Parent, Object, JSON::Ext::Generator::GeneratorMethods::Object, PP::ObjectMixin, Kernel, BasicObject]
Hoge
モジュールがGreet
モジュールのさらに下へ挿入されています。
複数のモジュールをprepend
した場合は一番下に書いたモジュールが優先されるのです。
prepend
したモジュールはクラス自身よりも先に探索される
モジュールAとBをprepend
したら「B → A → クラス → 親クラス」の順で探索する
extend
の使い方と特徴
extend
はモジュールのメソッドを特定のオブジェクトに対してのみ追加します。
include
やprepend
と異なり、クラス全体にはモジュールが追加されません。
他の2つとは性質が違うため、注意が必要です。
メソッドの探索順序
くりかえしになりますが、extend
はクラス全体にはモジュールが追加されません。
そのため探索順序の概念は適用されないです。
ではどのように動作するのか、具体的なコードで見ていきたいと思います。
extend
の具体例
extend
を使ってクラスにモジュールを取り込んでも、そのクラスから生成されたインスタンスはモジュールに定義されたメソッドを使えません。
module Greet
def hello
"Hello from Greet module"
end
end
class Child
extend Greet
end
child = Child.new
child.hello
#=> undefined method `hello' for an instance of Child (NoMethodError)
Child.ancestors # 継承チェーンに Greet は追加されていない
#=> [Child, Object, JSON::Ext::Generator::GeneratorMethods::Object, PP::ObjectMixin, Kernel, BasicObject]
extend
は特定のオブジェクトのみにメソッドを追加するからです。
上記の例だと、Child
クラスにGreet
モジュールのhello
メソッドが追加されたわけではありません。
Child
オブジェクトにのみモジュールが追加されたことになっています(Rubyではクラス自身もオブジェクト)
次のようにChild
クラスのクラスメソッドとしてhello
を実行すると、Greet
モジュールのhello
が呼び出されます。
module Greet
def hello
"Hello from Greet module"
end
end
class Child
extend Greet
end
Child.hello
#=> "Hello from Greet module"
Child
クラスのインスタンスにGreet
モジュールを追加したい場合、次のようにChild
オブジェクトに個別にextend
する必要があります。
module Greet
def hello
"Hello from Greet module"
end
end
class Child
end
child1 = Child.new
child1.extend Greet # child オブジェクトに個別で extend する
child1.hello
#=> "Hello from Greet module"
child2 = Child.new
child2.hello # child2 には extend していないため hello は使えない
#=> undefined method `hello' for an instance of Child (NoMethodError)
extend
したモジュールはそもそも継承チェーンに追加されない
extend
したオブジェクト自身のメソッドとしてのみ使える
クラスにモジュールをextend
したらクラスメソッドとしてのみ使える
include
, prepend
, extend
の使い分け
この3つの使い分けは次のようになるかなと思います。
-
include
: モジュールのメソッドをクラスのインスタンスメソッドとして追加したい -
prepend
: クラスのメソッドよりもモジュールのメソッドを優先したい or 上書きしたい -
extend
: 特定のオブジェクトにのみメソッドを追加したい(主にクラスメソッドを追加)
知っておくと業務ではもちろん、OSSのコードを読む際に役立つなと感じました。
参考資料