📲
【Ruby】include / prepend / extend の違いを理解する
2025-10-02 00:00
先輩エンジニアから紹介してもらった「メタプログラミングRuby 第2版」を読んでいる途中だが、学びがあるのでこまめに投稿してみようと思う。
module のメソッドを追加する手段として使用される include。
実はモジュールを追加する方法はこれだけではなく、他にも extend、prepend が存在している。
普段業務で使っているのは 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, ...]
