目次へ

4. Module#prepend

2013/03/01 シナジーマーケティング(株) 寺岡 佑起

4.1. Module#prependとは

Ruby2.0で追加されたModule#prependは、Module#includeと非常によく似た機能です。
Module#prependを使うと既存のクラスの処理を変更することができます。

Module#prependでメソッドを追加する

prependを利用するのに特に難しいことは必要ありません。
以下のようにprependにモジュールを指定するだけです。

module Greeting
  def greet
    puts "my name is #{name}!!"
  end
end

class Person
  prepend Greeting
  attr_accessor :name
end

obj = Person.new
obj.name = 'taro'
obj.greet # my name is taro!!

Personクラスにhelloメソッドを追加することができました。
しかし、この例ではprependをincludeに置き換えても動作は変わりません。

prependとincludeの違いはどこにあるのでしょうか。

4.2. prependとincludeの違い

prependの大きな特徴は、既存のメソッドをオーバーライド(上書き)できることにあります。
includeでは既存のクラスに存在するメソッドをオーバーライドできません。
同じモジュールをinclude、prependした場合でどのような違いがあるのかを確認してみます。

includeではメソッドをオーバーライドできない

先ほどのPersonクラスにgreetメソッドを定義し、Greetingモジュールは(prependではなく)includeします。

module Greeting
  def greet
    puts "my name is #{name}!!"
  end
end

class Person
  include Greeting
  attr_accessor :name

  def greet
    puts "nice to meet you."
  end
end

obj = Person.new
obj.name = 'jiro'
obj.greet # nice to meet you.

greetメソッドを呼び出すとPersonクラスに定義されたメソッドが呼び出されました。
これは、includeされたモジュールのメソッドよりPersonクラスに定義されたメソッドの優先順位が高いことを表しています。

prependでメソッドをオーバーライドする

では、includeをprependに変更して実行してみましょう。

module Greeting
  def greet
    puts "my name is #{name}!!"
  end
end

class Person
  prepend Greeting
  attr_accessor :name
  
  def greet
    puts "nice to meet you."
  end
end

obj = Person.new('taro')
obj.name = 'hanako'
obj.greet # my name is hanako!!

今度はGreetingモジュールで定義したメソッドが実行されました。
prependしたモジュールのメソッドはPersonクラスのメソッドより優先順位が高いことがわかります。

4.3. モジュールの仕組み

prependとincludeではメソッドの優先順位が異なることがわかりました。
この違いはどういった仕組みで実現されているのでしょうか。

Rubyのモジュールシステムについて振り返りながら、prependとincludeの仕組みの違いを確認します。

モジュールとクラス

Rubyでは、モジュールとクラスに構造上の大きな違いはありません。
Classクラスの親クラスはModuleとなっており、クラスの大半の機能はModuleクラスによって実現されています。
クラスとは、モジュールのようにincludeすることができない代わりに「クラスの継承」と「インスタンス化」の機能を追加したものと言えます。

includeはクラス継承の仕組みを上手く利用することにより実現されています。

ObjectとBasicObject

図1.はPersonクラスを定義した時の親クラスの継承を表しています。
クラス定義時に明示的に親クラスを指定しない場合、Objectクラスが親クラスになります。
Objectクラスのの親クラスはBasicObjectで、BasicObjectに親クラスはありません。

RubyのほとんどのクラスはObjectのサブクラスであり、全てのクラスはBasicObjectのサブクラスになります。
※BasicObjectはRuby1.9から導入さたクラスで、1.8以前ではObjectが全てのクラスの親クラスになります。

親クラスはClass#superclassメソッドで確認することができます。

Person.superclass      #=> Object
Object.superclass      #=> BasicObject
BasicObject.superclass #=> nil

モジュールのinclude

図2.はPersonクラスにGreetingモジュールをincludeした際の状態を表しています。
※Kernelモジュールは初めからObjectクラスにincludeされているモジュールです。

図2.の中で、それぞれのモジュールをincludeしたクラスと親クラスの間に置いているのには意味があります。
クラスとモジュールを左からたどるとメソッドの探索順(優先順位)になるためです。

includeと継承関係

図3.は図2.に継承関係(ancestors)の線を追加したものです。
この順序はClass#ancestorsメソッドで確認できます。

Person.ancestors  #=> [Person, Greeting, Object, Kernel, BasicObject]

Personをインスタンス化してgreetメソッドを呼び出した場合、以下の優先順位でメソッドが探索されます。

  • Person#greet
  • Greetingn#greet
  • Object#greet
  • Kernel#greet
  • BasicObject#greet
  • 存在しない場合は Person#method_missing、Greetingn#method_missing … と続く

モジュールがincludeされると、自クラスと親クラスの継承関係の間に挿入されます。
モジュールを複数includeすると、includeした順番に自クラスの後ろに挿入されます。

# 複数のモジュールをincludeする
module M1
end
module M2
end

class C
  include M1
  include M2
end

# 継承関係を表示
C.ancestors #=> [C, M2, M1, Object, Kernel, BasicObject]

モジュールのprepend

図4.はGreetingモジュールをprependした場合の継承関係の図です。
includeと違いPersonの左側に配置され、継承関係(ancestors)の線がGreetingモジュールから開始していることがわかります。

# prependの継承関係を確認
module Greeting
  # .....
end

class Person
  prepend Greeting
end

# 継承関係を表示
Person.ancestors #=> [Greeting, Person, Object, Kernel, BasicObject]

includeは「モジュールを自クラスと親クラスの継承関係の間に挿入する」機能だったのに対して、
prependは「モジュールを自クラスの継承関係の先頭に挿入する」機能になります。

prependされたモジュールのメソッドは対象のクラスよりも優先されることになります。
このため、モジュールをprependすることによって既存クラスのメソッドをオーバーライドすることができるのです。

4.4. モンキーパッチに利用する

Rubyではメソッドの定義を動的に追加、変更することができるため、既存のクラスの挙動を外部から変更することができます。
このような変更は「モンキーパッチ」と呼ばれ、不具合の修正や、既存の処理を拡張したい場合などに利用されます。

prependを利用することで、モジュールを使ったモンキーパッチができるようになります。
今まではalias_methodなどを利用したモンキーパッチの手法が主流でした。
※ alias_methodを用いたモンキーパッチについては Rails: alias_method_chain: 既存の処理を修正する常套手段 を御覧ください

prependを使ったモンキーパッチ

今回は、例としてIOクラスのeachメソッドを書き換えてみます。
前章でも利用したIO#eachは、一行ごとにブロックを実行するメソッドです。
この時ブロックに渡される一行の文字列の末尾には改行コードが付与されています。
eachメソッドをオーバーライドして、この改行コードを取り除いてみることにします。

# eachから改行コードを取り除くモジュール
module ChompedEach
  def each(*args)
    # 本来なら必要なeachメソッドにブロックが渡されなかった場合の処理を省略
    super(*args) {|line| yield line.chomp}
  end
end

IO.send(:prepend, ChompedEach)

# 改行区切りのhoge fuga piyoが記述されたファイルを作成
open('/tmp/hoge.txt') do |file|
  %w(hoge fuga piyo).each {|line| file.puts line }
end

open('/tmp/hoge.txt').each {|line| p line}
# --- 以下が出力される
# hoge
# fuga
# piyo

eachの中でsuperを呼び出しています。
このsuper呼び出しでは元々のIO#eachメソッドが実行されます。
これは、prependによってChompedEachモジュールがIOクラスの継承関係の先頭に挿入されているためです。

このように、prependを使って既存クラスの処理を拡張することができます。
prependを使ったモンキーパッチでは、パッチを当てるクラス側の処理はモジュールをprependするだけです。
汎用的なモジュールを定義しておくことで、複数のクラスで再利用することができます。

4.5. まとめ

Ruby2.0で追加されたModule#prependによって、再利用可能で構造化された手法でモンキーパッチを行うことが出来るようになります。
モンキーパッチは頻繁に利用すると可読性の低下などを招きますが、困ったときの奥の手として重宝されることが多いようです。
どうしても既存のメソッドの挙動変更が必要になった場合、Module#prependを使ってスマートなモンキーパッチを作ると良いでしょう。

↑このページの先頭へ

こちらもチェック!

PR
  • XMLDB.jp