rubyのオブジェクトのメソッド探索ルール
概要
最近、ActiveRecordのコードを読み始めてすぐにやめたわけです。
"super" つっても、どこの何を呼び出してるか全然わからんちんですよ。
それでまずは、表題の件(rubyでオブジェクトのメソッド探索ルール)について
調べてから立ち向かおうかと。
この辺を参考に。(本記事もこの辺を参考にオマージュしております) Rubyメソッド探索 [Ruby] オブジェクトのメソッド探索順序 - Qiita
まず、下記メソッドを沢山使いました。
# 継承ツリーを取得 <クラス>.ancestors
あと、本記事では使い忘れてしまったけど、下記メソッドも便利そうです。
# メソッドのオーナーを取得 <インスタンス>.method(:<メソッド名>).owner
本記事では、下記メソッドを使った場合のメソッド探索ルールについて調べてみます。
- extend
- include
- prepend
- 継承
まずは普通に継承してみる
# test.rb require 'rubygems' require 'bundler' Bundler.require require 'pry' class Super def method1 p "super#method1" end end class Sub < Super def method1 p "sub#method1" end end sub = Sub.new.method1
これは実行すると、当然の結果が返ってくる。
$ ruby ./test.rb "sub#method1"
次に、Subクラスの、method1を削除して実行する。(↓の状態)
# test.rb require 'rubygems' require 'bundler' Bundler.require require 'pry' class Super def method1 p "super#method1" end end class Sub < Super end sub = Sub.new.method1
えい。実行。
$ ruby ./test.rb "super#method1"
おお。これも当然。Subのancestorsをみてみると、どっちの場合も同じ。
[1] pry(main)> Sub.ancestors => [Sub, Super, Object, PP::ObjectMixin, Kernel, BasicObject]
includeをみてみる
次は、Greatモジュールを作って、Subクラスにincludeしてみた。
# test.rb require 'rubygems' require 'bundler' Bundler.require require 'pry' module Great def method1 p "great#method1" end end class Super def method1 p "super#method1" end end class Sub < Super include Great end sub = Sub.new.method1
これで実行。
$ ruby ./test.rb "great#method1"
今度は、Greatモジュールのが実行された。ancestorsは・・・
[1] pry(main)> Sub.ancestors => [Sub, Great, Super, Object, PP::ObjectMixin, Kernel, BasicObject]
おお。Superの手前に突っ込まれる、と。
prependをみてみる
次は、Fabulousモジュールを作って、Subクラスにprependしてみた。
$ ruby ./test.rb "fabulous#method1"
おおおお。更に前に突っ込まれた!ancestorsというと・・・
[1] pry(main)> Sub.ancestors => [Fabulous, Sub, Great, Super, Object, PP::ObjectMixin, Kernel, BasicObject]
おお。自分よりも前に突っ込まれている。prepend最強。
prependはruby2.0から実装された機能みたいです。
少し調べてみたところ、alias_method_chain撲滅できるぜ!なもののようです。
それが主目的で開発されたわけではないかもしれませんが。
» Ruby2.0のModule#prependは如何にしてalias_method_chainを撲滅するのか!? TECHSCORE BLOG
さてさて、ここまできたら最後だ。
extendをみてみる
extendは少し意味合いが違うメソッドな気がするけど、
似たようなことをやってるので、比べてみよう。
今度は、Miracleモジュールを作って、Subクラスにextendしてみた。
# test.rb require 'rubygems' require 'bundler' Bundler.require require 'pry' module Miracle def method1 p "miracle#method1" end end module Fabulous def method1 p "fabulous#method1" end end module Great def method1 p "great#method1" end end class Super def method1 p "super#method1" end end class Sub < Super include Great prepend Fabulous extend Miracle end Sub.new.method1 Sub.method1
実行すると、こんな感じ。
$ ruby ./test.rb "fabulous#method1" "miracle#method1"
Subクラスの特異メソッドとして動作している、と。
ancestorsは・・・
[1] pry(main)> Sub.ancestors => [Fabulous, Sub, Great, Super, Object, PP::ObjectMixin, Kernel, BasicObject]
あ、そうなのね。そうか。
instance method Object#extend (Ruby 2.0.0)
↑↑に、
" extend は、ある特定のオブジェクトだけにモジュールの機能を追加 したいときに使用します。 "
って書いてあるとおり、Subクラスのオブジェクトだけに機能が追加されてるから、
ancestorsに出てくるものではないのか。
でもメソッド探索上でいうと、特異メソッドなので、一番優先度が高い、と。
include/prependを何回も呼ぶと?
何回も呼んだときは、どれが優先度高くなるのかね。
include Great1 prepend Fabulous1 include Great2 prepend Fabulous2
こんなことをした場合。
# test.rb require 'rubygems' require 'bundler' Bundler.require require 'pry' module Fabulous1 def method1 p "fabulous1#method1" end end module Fabulous2 def method1 p "fabulous2#method1" end end module Great1 def method1 p "great1#method1" end end module Great2 def method1 p "great2#method1" end end class Super def method1 p "super#method1" end end class Sub < Super include Great1 prepend Fabulous1 include Great2 prepend Fabulous2 end Sub.new.method1
これで実行してみる。えいやー。
$ ruby ./test.rb "fabulous2#method1"
お、一番最後にprependしたFabulous2が呼ばれた。
そしてancestorsは・・・
[1] pry(main)> Sub.ancestors => [Fabulous2, Fabulous1, Sub, Great2, Great1, Super, Object, PP::ObjectMixin, Kernel, BasicObject]
おー。見事に後発優先ですな。
Fabulous2, Fabulous1
Great2, Great1
の順だもんね。なるほどなるほどー。勉強になりました。
ちなみに、少し規模が大きくなってくると、rubyは、include/prepend/extend/継承などなどの
嵐になるので、コード上書いてあるメソッドが、実際どこを呼び出してるか、
全然わかりません。なので、ちゃんとテストを書いて、ちゃんと動作するよね、
っていうのは最低限守らないと大変みたいです。
というわけで、今日はおしまい。