📲

【Ruby】include / prepend / extend の違いを理解する

2025-10-02 00:00

先輩エンジニアから紹介してもらった「メタプログラミングRuby 第2版」を読んでいる途中だが、学びがあるのでこまめに投稿してみようと思う。

メタプログラミングRuby 第2版
本書はRubyを使ったメタプログラミングについて解説する書籍です。メタプログラミングとは、プログラミングコードを記述するコードを記述することを意味します。前半では、メタプログラミングの背景にある基本的な考えを紹介しながら、動的ディスパッチ、ゴーストメソッド、フラットスコープといったさまざまな「魔術」を紹介します。後半では、ケーススタディとしてRailsを使ったメタプログラミングの実例を紹介します。今回の改訂では、Ruby 2とRails 4に対応し、ほぼすべての内容を刷新。Rubyを使ったメタプログラミングの魔術をマスターし、自由自在にプログラミングをしたい開発者必携の一冊です。
メタプログラミングRuby 第2版 favicon www.oreilly.co.jp
メタプログラミングRuby 第2版

module のメソッドを追加する手段として使用される include

実はモジュールを追加する方法はこれだけではなく、他にも extendprepend が存在している。

普段業務で使っているのは include で、極稀に prepend

正直 extend は使ったことがないかもしれない。

「モジュールを追加する = include」とさえ思っていたが、他にも選択肢があり、また使い方も異なることを学んだので書き残す。

各機能の動きを理解する

include

目的: モジュールのインスタンスメソッドをクラスに追加すること。

動作: 自クラスと親クラスの間に挿入するイメージ。

メソッド探索順: 自クラス → includeしたモジュール → 親クラス

prepend

目的: include と同じく、モジュールのインスタンスメソッドをクラスに追加すること。

動作: 自クラスの前に挿入するイメージ

メソッド探索順: prependしたモジュール → 自クラス → 親クラス

extend

目的: モジュールのインスタンスメソッドを特異メソッドとしてクラスが持つ特異クラスに追加すること。

動作: クラスの特異クラスと親クラスの特異クラスの間に挿入するイメージ(挿入場所は違えど、挿入タイミングはincludeに近い)

メソッド探索順: 自分の特異クラス → extendしたモジュール → 親の特異クラス

ここで補足を一点

特異クラス / 特異メソッドといったワードが出たが、これについては別記事で取り上げる。

実際に動かしてみる

include / prepend

子クラスの処理を実行したとき、親クラス・includeしたモジュール・prependしたモジュール、どういう順番で処理が動いていくかを見てみる。

まずは以下のコードを用意。

子クラスで include / prepend をしていて、それぞれのクラス / モジュールでcallメソッドを実装している。

callメソッドには super を仕込んでいるため、どの順番で @arr に文字列が追加されるかを見れるようになっている。

module IncludeMod
  def call
    @arr << "Include"
    super
  end
end

module PrependMod
  def call
    @arr << "Prepend"
    super
  end
end

class BaseClass
  def call
    @arr << "Base"
  end
end

class MyClass < BaseClass
  include IncludeMod
  prepend PrependMod

  def initialize
    @arr = []
  end

  def call
    @arr << "Self"
    super
  end
end

それでは callメソッドを実行してみる。

# call メソッドで、実際にどう処理が流れるかを確認
MyClass.new.call
# => ["Prepend", "Self", "Include", "Base"]

include / perpend の処理がどのタイミングで挿入されるかは「各機能の動きを理解する」で説明した通りの結果となっている。

ちなみに、わざわざ call メソッドを実装せずともメソッドの探索順序を確認できる方法がある。

それが .ancestors メソッドで、どの順番でクラス/モジュールが発見されるかを確認することができる。

実際に実行してみると、MyClass より前に PrependMod が挿入され、MyClass と BaseClass の間に IncludeMod が挿入されることが確認できる。

# クラス・モジュールのメソッド探索順序を確認
MyClass.ancestors
# =>
# [
#   PrependMod,
#   MyClass,
#   IncludeMod,
#   BaseClass,
#   ~~ 中略 ~~
#   Kernel,
#   BasicObject
# ]

extend

次に extend の動きを見ていく。

基本的には先程と同じ。

class_callメソッドを実装していて、どの順番で @class_arr に文字列が入るかを確認できるようになっている。

@class_arr は自動でリセットされないので、一度 class_call を実行したら MyClass.reset で初期化を推奨。

module ExtendMod
  def class_call
    @class_arr ||= []
    @class_arr << "ExtendMod"
    super if defined?(super)
    @class_arr
  end
end

class MyClass
  extend ExtendMod

  def self.class_call
    @class_arr ||= []
    @class_arr << "MyClass"
    super if defined?(super)
    @class_arr
  end

  # 初期化用
  def self.reset
    @class_arr = []
  end
end

それでは class_call メソッドを実行してみる。

MyClass.class_call
# => ["MyClass", "ExtendMod"]

こちらも「各機能の動きを理解する」で説明した通りの結果となっている。

.ancestors メソッドの動きも見ていくと、MyClass の後に ExtendMod が挿入されていることが確認できる。

# 特異クラスを見るときは、 MyClass.singleton_class を使用する
MyClass.singleton_class.ancestors
# =>
# [
#   #<Class:MyClass>,
#   ExtendMod,
#   ~~ 中略 ~~
#   Kernel,
#   BasicObject
# ]

各機能共通のポイント

共通するポイントとしては、以下。

  • インスタンスメソッド を全て取り込むこと。
  • クラスメソッド は常に無視すること。

インスタンスメソッドの取り込みについてはこれまで見てきた通り。

クラスメソッドの振る舞いについて、これから見ていく。

クラスメソッドは常に無視する

module IncludeMod
  def self.call
    "IncludeMod"
  end
end

module PrependMod
  def self.call
    "PrependMod"
  end
end

module ExtendMod
  def self.call
    "ExtendMod"
  end
end

class MyClass
  include IncludeMod
  prepend PrependMod
  extend ExtendMod
end

# クラスメソッドの call を実行
MyClass.call
# undefined method 'call' for class MyClass (NoMethodError)

include / prepend / extend でそれぞれ取り込んでみたが、self.call は undefined method となった。

ここで分かる通り、インスタンスメソッドのみが取り込まれ、クラスメソッドは無視される動きになる。

おまけ

extend で prepend 的な動きは実現できるの?

可能。ただし、これまでの方法とは少し異なる手法が必要。

結論、 Class.singleton_class.prepend(PrependModule) が必要。

module ExtendMod; end

module ClassPrependMod; end

class MyClass
  extend ExtendMod
end

# 現時点の探索順序を確認
MyClass.singleton_class.ancestors
# => [#<Class:MyClass>, ExtendMod, ...]

# POINT!!
MyClass.singleton_class.prepend(ClassPrependMod)

# 再度、探索順序を確認
MyClass.singleton_class.ancestors
# => [ClassPrependMod, #<Class:MyClass>, ExtendMod, ...]

この記事をシェアする

© 2025 tatsuya-develop