お久しぶりです。寺岡です。
lazyについて書いた前回に続いて、Ruby2.0について書いてみたいと思います。
今回注目する新機能は、Module#prependです。
Module#prependはRuby2.0で新たに追加された、Module#includeの親戚のような機能です。
一言で表すと「クラスの継承階層の手前にモジュールを追加する」ことができるようになります。
ActiveSupportのMudule#alias_method_chainを使わずに綺麗なモンキーパッチ実装することができる、Module#prependの挙動を探ってみたいと思います。
ruby2.0-rc1のインストール
まずは実行環境の準備です。
前回の記事ではruby2.0-preview2を使いましたが、折角なのでruby2.0-rc1にバージョンアップを行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ rvm get head $ rvm get latest $ rvm list known | grep '\[ruby-\]' [ruby-]1.8.6[-p420] [ruby-]1.8.7[-p371] [ruby-]1.9.1[-p431] [ruby-]1.9.2[-p320] [ruby-]1.9.3-p125 [ruby-]1.9.3-p194 [ruby-]1.9.3-p286 [ruby-]1.9.3-[p327] [ruby-]1.9.3-head [ruby-]2.0.0-rc1 $ rvm install 2.0.0-rc1 $ rvm use ruby-2.0.0-rc1 $ ruby -v ruby 2.0.0dev (2013-01-07 trunk 38733) [x86_64-linux] |
alias_method_chainってなんだっけ?
Module#prependを試す前に、alias_method_chainがどんな機能かを確認してみましょう。
alias_method_chainはActiveSupportに用意されているメソッドで、パッチを当てる際などによく利用されるメソッドです。
今回は以下のHogeクラスのhelloメソッドを、taro君向けにカスタマイズしてみます。
1 2 3 4 5 6 |
# hoge.rb class Hoge def hello "hello" end end |
オープンクラスで再定義
Rubyには「メソッドの動的な定義、書き換えが可能」という大きな特徴があるため、オープンクラスを利用してメソッドを上書きすることで、メソッドの挙動を変更することができます。
1 2 3 4 5 6 7 8 9 |
load 'hoge.rb' # オープンクラスで元のメソッドをtaro君向けに書き換える class Hoge def hello "hello taro!" end end puts Hoge.new.hello # 「hello taro!」 と出力される |
メソッドを上書きすることで、望みどおりの結果を得ることができました。
この例では、既存のメソッドを完全に置き換えてしまっているため、helloメソッドの出力が「Hello!」に変更された場合など、元々のhelloメソッドの変更に追従できない欠点があります。
aliasで別名をつける
そこで、メソッドの別名をつけるaliasを利用して元のメソッドを別名で退避させ、書き換えたメソッドから呼び出せるようにするテクニックが生まれました。
1 2 3 4 5 6 7 8 9 10 11 |
load 'hoge.rb' # 元々のhelloメソッドを利用しつつtaro君向けに書き換えたい! class Hoge def hello_with_taro hello_without_taro + " taro!" end alias hello_without_taro hello alias hello hello_with_taro end puts Hoge.new.hello # 「hello taro!」 と出力される |
alias_method_chainでDRYに!
上の例の2行のalias呼び出しを簡略化してくれるのがalias_method_chainです。
1 2 3 4 5 6 7 8 9 10 |
load 'hoge.rb' # alias_method_chainでhelloメソッドをtaro君向けに書き換えたい! class Hoge def hello_with_taro hello_without_taro + " taro!" end alias_method_chain :hello, :taro end puts Hoge.new.hello # 「hello taro!」 と出力される |
このalias_method_chainはRailsのソースコード中の至る所で利用されています。
しかし、メソッドを別名で退避させて呼び出すこのテクニックはあまり綺麗な手段とは言えませんでした。
prependの登場!
そこで満を持して登場したのがMudule.prependです。
早速上記のhelloメソッドを置き換えてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
load 'hoge.rb' # Mudule.prepend利用してtaro君向けに書き換えたい! module TarosHello def hello super + " taro!" end end class Hoge prepend TarosHello end puts Hoge.new.hello # 「hello taro!」 と出力される |
見事に書き換えることができました。
モジュールをprependですることで、TarosHello#helloの中からsuper(Hoge#hello)を呼び出すことが出来ています。
includeの仕組み
試しに、先ほどの例のprependをincludeに変更してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
load 'hoge.rb' # Mudule.include利用してtaro君向けに書き換えられない…… module TarosHello def hello super + " taro!" end end class Hoge include TarosHello end puts Hoge.new.hello # 「hello」 と出力される |
Hogeクラスの元々のhelloメソッドが実行され、TarosHelloモジュールのhelloは実行されませんでした。
では、この違いはどこから来るのでしょうか?
includeのおさらい
includeとprependの違いを理解するために、includeの挙動をおさらいしてみます。
includeとは、クラスの継承関係の間にモジュールを追加するメソッドです。
includeされたモジュールは、対象クラスと親クラスの間に追加されることになります。
Muduleをincludeした際の継承関係の例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
module ModuleX def hoge "ModuleX" end end module ModuleY def hoge "ModuleY," + super end end module ModuleZ include ModuleY def hoge "ModuleZ," + super end end class MyClassA include ModuleX include ModuleZ def hoge "MyClassA," + super end end MyClassA.new.hoge # "MyClassA,ModuleZ,ModuleY,ModuleX" MyClassA.ancestors # [MyClassA, ModuleZ, ModuleY, ModuleX, Object, Kernel, BasicObject] |
ancestorsは、モジュールの継承関係を配列で返してくれるメソッドです。
MyClassA.new.hogeメソッドの結果と見比べてわかるように、実行時にはこの順番でメソッドの探索が行われます。
インクルードされたモジュールは、自身のクラス継承関係の一つ上に追加されます。
モジュールを利用してのmix-inは多重継承のように振舞いますが、実際には単一継承と同じように線形の継承ルールを持っているのです。
MyClassAの継承関係の図(include)
prependに迫る!
では本題のModule#prependに迫って行きましょう。
下の例ではincludeの例にあったModuleZをprependするように変更しました。
Muduleをprependした際の継承関係の例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
module ModuleX def hoge "ModuleX" end end module ModuleY def hoge "ModuleY," + super end end module ModuleZ include ModuleY def hoge "ModuleZ," + super end end class MyClassA include ModuleX prepend ModuleZ def hoge "MyClassA," + super end end MyClassA.new.hoge # "ModuleZ,ModuleY,MyClassA,ModuleX" MyClassA.ancestors # [ModuleZ, ModuleY, MyClassA, ModuleX, Object, Kernel, BasicObject] |
prependされたモジュールが継承関係上、対象のクラスより手前に位置していることがわかります。
このため、モジュールのメソッドからsuperを呼び出すことで元々のメソッドを呼び出すことができるわけです。
MyClassAの継承関係の図(prepend)
includeとprependの使いわけ
includeとprependの継承関係の図を比べてみると、includeはクラスの奥に、prependはクラスの手前にモジュールが追加されているのがわかります。
このため、prependされたモジュールに定義されたメソッドは、対象のクラス(この場合はMyClassA)で定義されたメソッドより優先されます。
includeとprependでは継承関係の挿入位置の違いによって、以下の特徴を持ちます。
include
- モジュールで定義したメソッドでクラスに存在するメソッドの上書きはできない
- モジュールによって追加されるメソッドをクラス側で上書きできる
prepend
- モジュールで定義したメソッドでクラスに存在するメソッドを上書きできる
- モジュールによって追加されるメソッドはクラス側では上書きできない
これらの特徴からincludeとprependは以下のように使い分ければ良いことがわかります。
includeはクラスに対して新たな機能を提供する場合に利用する。
prependはクラスに既に存在する機能を変更するために利用する。
まとめ
今回はprependについて追いかけてみました。
改めて違いを比べてみたら、明確な使い分けが見えてきて面白かったです。
次回はRefinementsについて書いてみようかと思います。