xengineer’s diary

結果、メモ的な内容になっています。

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/継承などなどの

嵐になるので、コード上書いてあるメソッドが、実際どこを呼び出してるか、

全然わかりません。なので、ちゃんとテストを書いて、ちゃんと動作するよね、

っていうのは最低限守らないと大変みたいです。

というわけで、今日はおしまい。