0
0

【Ruby】include, prepend, extendの違いと使い分け

Posted at

はじめに

クラスにモジュールを取り込む方法として、include, prepend, extendの3つがあります。

この記事では3つの方法の違いと使い分けについてまとめていきたいと思います。

なお、この記事におけるバージョンはRuby 3.3です。

そもそもモジュールとは?

モジュールは関連するメソッドや定数をグループ化したものです。

クラスと似ていますが、以下の2点で異なります。

  • モジュールはインスタンス化できない
  • モジュールは継承できない

複数のクラスに共通の機能を提供したり、名前空間を分割してメソッド名の衝突を避けたりしたい場合に用いられます。

includeの使い方と特徴

includeはモジュールのメソッドをクラスへ取り込むために使われます。

メソッドの探索順序

includeを使うとモジュールがメソッドの探索順序に組み込まれます。

その結果、次の順序でメソッドが探索されます。

  1. クラス自身に定義されているメソッド
  2. includeされたモジュールのメソッド
  3. スーパークラスのメソッド

メソッドの探索順序において、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の使い方と特徴

prependincludeと同じく、モジュールをクラスへ取り込むために使います。

メソッドの探索順序

prependを使った時のメソッド探索の順序は次のようになります。

  1. prependされたモジュールのメソッド
  2. クラス自身に定義されているメソッド
  3. スーパークラスのメソッド

探索順序において、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はモジュールのメソッドを特定のオブジェクトに対してのみ追加します。

includeprependと異なり、クラス全体にはモジュールが追加されません。

他の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のコードを読む際に役立つなと感じました。

参考資料

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0