4. Module#prepend
2013/03/01 シナジーマーケティング(株) 寺岡 佑起
[Ruby 2.0] 第4章 Module#prepend
- 4.1. Module#prependとは
- 4.2. prependとincludeの違い
- 4.3. モジュールの仕組み
- 4.4. モンキーパッチに利用する
- 4.5. まとめ
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を使ってスマートなモンキーパッチを作ると良いでしょう。