Ruby2.0のModule#prependは如何にしてalias_method_chainを撲滅するのか!?


お久しぶりです。寺岡です。
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にバージョンアップを行います。

alias_method_chainってなんだっけ?

Module#prependを試す前に、alias_method_chainがどんな機能かを確認してみましょう。
alias_method_chainはActiveSupportに用意されているメソッドで、パッチを当てる際などによく利用されるメソッドです。
今回は以下のHogeクラスのhelloメソッドを、taro君向けにカスタマイズしてみます。

オープンクラスで再定義

Rubyには「メソッドの動的な定義、書き換えが可能」という大きな特徴があるため、オープンクラスを利用してメソッドを上書きすることで、メソッドの挙動を変更することができます。

メソッドを上書きすることで、望みどおりの結果を得ることができました。
この例では、既存のメソッドを完全に置き換えてしまっているため、helloメソッドの出力が「Hello!」に変更された場合など、元々のhelloメソッドの変更に追従できない欠点があります。

aliasで別名をつける

そこで、メソッドの別名をつけるaliasを利用して元のメソッドを別名で退避させ、書き換えたメソッドから呼び出せるようにするテクニックが生まれました。

alias_method_chainでDRYに!

上の例の2行のalias呼び出しを簡略化してくれるのがalias_method_chainです。

このalias_method_chainはRailsのソースコード中の至る所で利用されています。
しかし、メソッドを別名で退避させて呼び出すこのテクニックはあまり綺麗な手段とは言えませんでした。

prependの登場!

そこで満を持して登場したのがMudule.prependです。
早速上記のhelloメソッドを置き換えてみましょう。

見事に書き換えることができました。
モジュールをprependですることで、TarosHello#helloの中からsuper(Hoge#hello)を呼び出すことが出来ています。

includeの仕組み

試しに、先ほどの例のprependをincludeに変更してみます。

Hogeクラスの元々のhelloメソッドが実行され、TarosHelloモジュールのhelloは実行されませんでした。
では、この違いはどこから来るのでしょうか?

includeのおさらい

includeとprependの違いを理解するために、includeの挙動をおさらいしてみます。

includeとは、クラスの継承関係の間にモジュールを追加するメソッドです。
includeされたモジュールは、対象クラスと親クラスの間に追加されることになります。

Muduleをincludeした際の継承関係の例

ancestorsは、モジュールの継承関係を配列で返してくれるメソッドです。
MyClassA.new.hogeメソッドの結果と見比べてわかるように、実行時にはこの順番でメソッドの探索が行われます。
インクルードされたモジュールは、自身のクラス継承関係の一つ上に追加されます。
モジュールを利用してのmix-inは多重継承のように振舞いますが、実際には単一継承と同じように線形の継承ルールを持っているのです。

MyClassAの継承関係の図(include)

module

prependに迫る!

では本題のModule#prependに迫って行きましょう。
下の例ではincludeの例にあったModuleZをprependするように変更しました。

Muduleをprependした際の継承関係の例

prependされたモジュールが継承関係上、対象のクラスより手前に位置していることがわかります。
このため、モジュールのメソッドからsuperを呼び出すことで元々のメソッドを呼び出すことができるわけです。

MyClassAの継承関係の図(prepend)

prepend

includeとprependの使いわけ

moduleprepend

includeとprependの継承関係の図を比べてみると、includeはクラスの奥に、prependはクラスの手前にモジュールが追加されているのがわかります。
このため、prependされたモジュールに定義されたメソッドは、対象のクラス(この場合はMyClassA)で定義されたメソッドより優先されます。
includeとprependでは継承関係の挿入位置の違いによって、以下の特徴を持ちます。

include

  • モジュールで定義したメソッドでクラスに存在するメソッドの上書きはできない
  • モジュールによって追加されるメソッドをクラス側で上書きできる

prepend

  • モジュールで定義したメソッドでクラスに存在するメソッドを上書きできる
  • モジュールによって追加されるメソッドはクラス側では上書きできない

これらの特徴からincludeとprependは以下のように使い分ければ良いことがわかります。
includeはクラスに対して新たな機能を提供する場合に利用する。
prependはクラスに既に存在する機能を変更するために利用する。

まとめ

今回はprependについて追いかけてみました。
改めて違いを比べてみたら、明確な使い分けが見えてきて面白かったです。
次回はRefinementsについて書いてみようかと思います。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です