Rubyのアラウンドエイリアスとメソッドラッパー
アラウンドエイリアス
Rubyでは、メソッドにエイリアス(別名)をつけることができます。
# sample.rb class Sample def sample_method "abcdefg" end alias_method :old_sample, :sample_method end obj = Sample.new puts obj.sample_method # => "abcdefg" puts obj.new_method # => "abcdefg"
alias_methodメソッドでは、一番目のシンボルにエイリアスを設定し、二番目のシンボルには元のメソッドの名前を入力します。 上の例では、sample_methodの別名にnew_methodが設定されたため、 obj.sample_methodとobj.new_methodは同じメソッドを実行しています。
このエイリアスの実用的な使い方として、アラウンドエイリアスという手法が存在します。
# sample.rb class Sample def output; 10 * 10; end alias_method :old_output, :output end sample = Sample.new puts sample.output # => 100 Sample.class_eval do def output; old_output * 10; end end puts sample.output # => 1000
アラウンドエイリアスとは、新しいメソッドで、古いメソッドを覆う(ラップする)ことです。 上記のコードの場合、エイリアスとして付与した名前を古いメソッドとして捉え、 元のメソッドの名前を新しいメソッドとして捉えます。
ん、逆なんじゃないの? みたいな感覚になるかもしれませんが、 エイリアスは、元のメソッドの、保存用の複製と捉えてみてください。 複製元のメソッドは更新用として存在し、その中のコードを変更するのです。
そのため、元のメソッドのコードをいじるわけですから、それを新しいメソッドであると捉えるのには、少し抵抗があるかもしれません。 しかし、中身は更新されるわけですから、名前は同じでも、元のメソッドではないのです。 元のメソッドの中身は新しく命名したエイリアスのメソッドの中にあります。
# alias_method.rb alias_method :old, :new :old # => メソッドのエイリアス。この中に元のメソッド(new)の内容が複製される。 :new # => 元のメソッド。この中に新しいメソッドのコードを書く(複製しているからエイリアスメソッドを呼び出すことで、元のメソッドの内容を復元できる)。
このように、古いメソッドを新しいメソッドの中で呼ぶことをアラウンドエイリアス(古いメソッドであるエイリアスメソッドのアラウンドを新しいメソッドで囲っているので)と呼びます。
アラウンドエイリアスは、 新しく作り直すメソッドの中で、元のメソッドで得られる戻り値が必要な時 に使えます。
メタプログラミングRubyの例を載せると(第二版138ページ参照)、
# sample.rb class String alias_method :real_length, :length def length real_length > 5 ? "long" : "short" end end "War and Peace".length # => "long" "War and Peace".real_length # => 13
ここで新しく作り直したlengthメソッドは、もともと、文字列の長さを返すメソッドでした。 しかし、上のコードでは、文字列の長さが5文字以上なら、"long"を、5文字以下なら"short"を返すメソッドに更新されています。
このメソッドを作成するには、もともとのlengthメソッドに備わっていた、文字列の長さを返すロジックが必要になってきます。 すでにalias_methodでもともとのlengthメソッドの中身をreal_lengthに複製しているので、lengthメソッドの中でreal_lengthを呼び出せば、それが文字列の長さを計算してくれます。 real_methodの戻り根を5以上かどうかを判断し、Trueなら"long"、Falseなら"short"を返すように(ここでは三項演算子を使っています)、lengthの中身を書き換えると、無事、「文字列の長さが5文字以上なら、"long"を、5文字以下なら"short"を返すメソッド」を定義することができます。
これらの作業を上のコードでは実践しているわけです。
メソッドラッパー
アラウンドエイリアスのように、メソッドでメソッドを包み込む技法のことを、 メソッドラッパーと呼びます。
メソッドラッパーには、アラウンドエイリアスのほか、refinements(改良化)と、prependメソッドを利用したものがあります。
refinements
まずはrefinementsについて説明します。
# sample.rb class Sample def a_method "original method" end end sample = Sample.new puts sample.a_method # => "original method" module RefineSample refine Sample do def a_method "refined method" end end end using RefineSample puts sample.a_method # => "refined method"
refinementsは、すでに存在するクラスのメソッドを参照して、そのメソッドの中身を一時的に変更します。 上記のコードでは、既存のクラスSampleのa_methodメソッドを、 moduleの中で展開したrefineメソッドのブロックの中で書き換えています。
しかし、このままではa_methodメソッドの内容は更新されません。 このmoduleのrefineの使用宣言をしなければならないのです。
使用宣言は、
# sample.rb using ModuleName
で行えます。 この宣言以降にa_methodを呼び出すと、その内容は以前のものから変化しています。 refineの有効範囲は、usingを使って使用宣言箇所から、そのスコープの終わりまであります。
先ほどの例であれば、ファイルが終わるまでですが、 moduleやclassの中でusing使用宣言をすると、そのmoduleやclassの終わりまで、refinementsは有効です。
このrefinementsもメソッドラッパーとして利用できるのです。 メタプログラミングRubyの説明がわかりやすいので引用します(第二版140ページ参照)。
# sample.rb module StringRefinements refine String do def length super > 5 ? "long" : "short" end end end using StringRefinements puts "War and Peace".length # => "long"
refineのブロックの中に、既存のlengthメソッドを記述し、新しいロジックを書き込みます。 このとき、メソッドのスコープ内でsuperを呼び出すと、もともとのlengthメソッドに備わっていたロジックもよみがえります。
つまり、上記のコードでは、 新しいlengthメソッドの中に古いlengthメソッドを、superという記述で呼び出し、新しいロジックの中に組み込んだ、 ということになります。
古いメソッドを新しいメソッドで覆っている(ラップしている)ので、これはメソッドラッパーです。
Module#prepend
さらにもう1つ、メソッドラッパーの性質をもつメソッドが存在します。 それはprependメソッドです。
prependメソッドはincludeメソッドの変化型です。 includeとは少し動きが異なります。
includeは、ご存知の通り、moduleを読み込むメソッドです。
# sample.rb module ModuleSample def calc 1 + 1 end end class Sample include ModuleSample end sample = Sample.new puts sample.calc # => 2
includeしたmoduleは、この場合、インクルーダー(includerはincludeしたものの意味。したがってこの場合はSampleクラス)の上に挿入されます。 以下のコードを調べてみるとすぐにわかります。
# sample.rb # 先ほどの続き。 p Sample.ancestors # => [Sample, ModuleSample, Object, Kernel, BasicObject]
ancestors(先祖)メソッドは、Sampleが継承しているmoduleとclassを、一番最初のクラスであるBasicObjectまでさかのぼります。 早い話がレシーバーの先祖を若い順から参照することができるわけです。
includeすると、ancestorsメソッドによれば、Sampleの1つ上にModuleSampelモジュールが挿入されていることがわかります。 しかし、prependでは、これと挙動が異なってきます。
# sample.rb module ModuleSample def calc 1 + 1 end end class Sample prepend ModuleSample end sample = Sample.new puts sample.calc # => 2
モジュールを参照して、計算結果を出力する点では、includeと同一の動きをします。 しかし、
# sample.rb # 先ほどの続き。 p Sample.ancestors # => [ModuleSample, Sample, Object, Kernel, BasicObject]
先祖たちの順番が変わってしまいました。
ここからわかるように、prependメソッドでモジュールを利用すると、そのモジュールはインクルーダーの下にやってくるのです。 includeメソッドを使うと、モジュールはインクルーダーの上にやってきていましたが、インクルーダーの下へやってくるprependの利用価値とはなんなのでしょうか?
それは、インクルーダーのメソッドをオーバーライドできる点にあります。
ModuleSampleモジュールがSampleクラスの上にあれば、ModuleSampleの中でSampleのメソッドを呼び出すことはできません。クラスもモジュールも、上のクラスやモジュールからしかメソッドを継承できないからです。 そう考えると、下にいるモジュールは、上にいるクラスのメソッドを参照できるはずです。
prependメソッドによって、インクルーダーであるクラスの下に挿入されたモジュールも、もちろんそのインクルーダーのメソッドを参照できるのです。
# sample.rb class Sample def calc 10 + 10 end end module ModuleSample def calc # => Sampleクラスのcalcメソッドを上書き。 20 + 20 end end Sample.class_eval do prepend ModuleSample # => モジュールをprependメソッドでインクルードする。 end sample = Sample.new puts sample.calc # => 40 (Sampleクラスのcalcメソッドではなく、ModuleSampleモジュールのcalcメソッドを返している) p Sample.ancestors # => [ModuleSample, Sample, Object, Kernel, BasicObject]
prependメソッドは、このような動きを利用して、メソッドラッパーの効果を発揮します。 以下はメタプログラミングRubyから引用したコードです(第二版141ページ参照)。
# sample.rb # Stringクラスはデフォルトで存在するので書きません。 module ExplicitString def length super > 5 ? "long" : "short" end end String.class_eval do prepend ExplicitString end "War and Peace".length # => "long"
上記のコードでも、refinementsと同様に、新しいlengthメソッドの中で古いlengthメソッドの戻り値をsuperで呼び出しています。 つまり、メソッドラッパーなのです。
3つのメソッドラッパー
ここではメソッドラッパーを3種類紹介しました。
- アラウンドエイリアス
- refinementsラッパー
- prependラッパー
これらは、古いメソッドを上書きするときに、元のロジックを残しながら、新しく更新するロジックを定義し、また、その古いロジックを新しいロジックの中で再利用する手段を与えてくれています。
初学者からすると混乱してしまう要因の1つとなりえそうですが、コードを何度か書いてみると、徐々に実感していけると思います。