[ Ruby ] 継承ツリーについてメソッドが探索される流れを見ながら調べてみる

#はじめに
メソッドを呼ぶ際に、そのメソッドは自身のクラス内に定義されている場合の他にも
スーパークラス内や、module、又はクラスから作成されたインスタンスに特異メソッドとして定義されているなど
様々なケースがあるかと思います。
今回は、それらのケースの、メソッドの探索の流れについて
まとめていきたいと思います。

## 自身のメソッド

```.rb
class Klass
  def hello
    puts "Hello_from_Klass"
  end
end

klass_obj = Klass.new
klass_obj.hello
# => Hello_from_Klass
```

最も単純なケースかと思われます。
`obj`オブジェクトに対して`hello`メソッドが呼ばれた際、
直接のクラスであるKlassクラスを参照し、そこに`hello`メソッドがないかを探します。
今回の場合は、そこで見つけることが出来たので、そこで探索は終了しそのメソッドを実行します。


![スクリーンショット 2020-11-14 22.35.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/562008/9254971f-e080-f0b4-3c38-0491d2b543ca.png)

## スーパークラスのメソッド

```.rb
class Klass
  def hello
    puts "Hello from Klass"
  end
end

class SubKlass < Klass
end

sub_klass_obj = SubKlass.new
sub_klass_obj.hello
# => Hello_from_Klass
```

探索方向は、まず直接のクラスを参照し、そこになければ
その親クラス、なければ更にその親クラス・・・
のようにクラスの継承順序と等しくなります。
今回のケースでは、`SubKlass`を参照し`hello`メソッドが無かったため
その親クラスである`Klass`を参照し、`hello`メソッドが定義されていたため
探索は終了し、メソッドを実行します。

![スクリーンショット 2020-11-14 22.50.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/562008/bc33ee3e-4494-6cc1-4979-01416769e7f5.png)

## 特異メソッド

```.rb
class Klass
  def hello
    puts "Hello from Klass"
  end
end

obj = Klass.new

def obj.hello
  puts "Hello from singleton"
end

obj.hello
# => Hello from singleton
```

このケースでは、直接のクラスに定義した`hello`メソッドではなく
特異メソッドとして定義した`hello`メソッドが呼ばれました。
つまり、特異クラスはオブジェクトのクラスより先にメソッドが探索されていることが
わかります。


![スクリーンショット 2020-11-15 13.31.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/562008/3dca7643-aec7-9844-b3c4-45699393fc89.png)

## クラスにmixinされたメソッド
スーパークラスとmoduleに同名メソッドを定義して
サブクラスのインスタンスからそのメソッドを呼んでみます。

```.rb
module HelloModule
  def hello
    puts "Hello from HelloModule"
  end
end

class Klass
  def hello
    puts "Hello from Klass"
  end
end

class SubKlass < Klass
  include HelloModule
end


sub_klass_obj = SubKlass.new
sub_klass_obj.hello
# => Hello from HelloModule
```
includeしたmoduleの`hello`メソッドが呼ばれました。
includeされたmodulueは、includeしたクラスの親クラスとの間に
存在することになります。
この時、moduleをラップしたかのような無名クラスが作られ継承ツリーの中に埋め込まれています。
ちなみに、この無名クラスはRubyのプログラムからは触れることが出来ません。

このように無名クラスが作成され、継承ツリーに埋め込まれるため、
メソッドの探索も通常の継承が行われている時と同じルールで参照することができています。

![スクリーンショット 2020-11-15 14.03.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/562008/13644192-d762-5bbb-4a46-d473c36257ea.png)


## 複数のmoduleをinclude
先ほどは、1つのmoduleのみをincludeさせましたが、
moduleのincludeはクラスの継承と異なり
1つのクラスに対して複数のmoduleをincludeすることが出来ます。

実際に複数のmoduleをincludeさせてメソッドを呼んでみます。

```.rb
module FirstModule
  def hello
    puts "Hello from FirstModule"
  end
end

module SecondModule
  def hello
    puts "Hello from SecondModule"
  end
end

class Klass
  def hello
    puts "Hello from Klass"
  end
end

class SubKlass < Klass
  include FirstModule
  include SecondModule
end


sub_klass_obj = SubKlass.new
sub_klass_obj.hello
# => Hello from SecondModule
```

複数のmoduleをincludeさせた場合、
最後にincludeさせたmoduleのメソッドが呼ばれました。
このように複数のmoduleを同一のクラスにincludeさせた場合、
includeさせた順にmoduleはそのクラスの継承ツリーに埋め込まれていきます。

まず、FirstModuleがincludeされ以下の継承ツリーになります。
![スクリーンショット 2020-11-15 14.18.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/562008/b6924540-3254-ebea-da41-5a278c44ccae.png)

その後、下に記述してあるSecondModuleがincludeされると、以下のようになります。
![スクリーンショット 2020-11-15 14.20.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/562008/cbc32906-452a-1b1e-af54-220067fd9cfa.png)


#### moduleをincludeした後に、再度include
今度は、先ほどのように`FirstModule``SecondModule`の順に読み込んだ後に
再度`FirstModule`を読み込んでみます。

```.rb
 module FirstModule
  def hello
    puts "Hello from FirstModule"
  end
end

module SecondModule
  def hello
    puts "Hello from SecondModule"
  end
end

class Klass
  def hello
    puts "Hello from Klass"
  end
end

class SubKlass < Klass
  include FirstModule
  include SecondModule
  include FirstModule
end


sub_klass_obj = SubKlass.new
sub_klass_obj.hello
# => Hello from SecondModule
```
最後にincludeの記述があるのは`FirstModule`ですが
呼ばれたのは`SecondModule`のメソッドでした。

実は継承ツリーにmoduleをラップしたクラスを差し込む際に、
継承ツリーに同じmoduleがないかを確認しています。
そのため、２回目の`include FirstModule`が呼ばれた際に、
既に`FirstModule`がincludeされているため
差し込まれずにincludeメソッドが終了します。

## moduleをprepend

```.rb
module PrependModule
  def hello
    puts "Hello from PrependModule"
  end
end

module IncludeModule
  def hello
    puts "Hello from IncludeModule"
  end
end

class Klass
  prepend PrependModule
  include IncludeModule
  def hello
    puts "Hello from Klass"
  end
end

klass_obj = Klass.new
klass_obj.hello
# => Hello from PrependModule
```
includeはmoduleをラップした無名クラスを
includeしたクラスより上位に埋め込むのに対して
prependはそのクラスより下位に埋め込まれます。(prependしたクラスより優先的にメソッドが呼ばれる)
そのため、prependするmodule内で`super`の呼び出しを行うと、
prependしたクラスの同名メソッドが呼ばれます。

![スクリーンショット 2020-11-15 16.51.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/562008/93733e67-ea7c-03dc-7fb3-9379e56f10dd.png)

## オブジェクトに存在しないメソッドを呼び出す

```.rb
module HelloModule
  def hello
    puts "Hello from FirstModule"
  end
end

class Klass
  def hello
    puts "Hello from Klass"
  end
end

class SubKlass < Klass
  include HelloModule
end

obj = SubKlass.new
obj.bye
# => undefined method `bye' for #<SubKlass:0x00007fb5fb80c628> (NoMethodError)
```

オブジェクトのどこにも存在しない`bye`メソッドを呼んでみたところ
`NoMethodError`を吐きました。

byeメソッドが呼ばれると継承ツリーを順に辿ってそのメソッドを探していきます。
しかし、今回はどこにも定義していないため継承ツリーの最上位である`BasicObject`まで探した後
直接のクラスである`SubKlass`まで戻り、今度は`method_missing`メソッドを継承ツリーの順番に探していきます。
今回は自分で`method_missing`メソッドを定義していないため最上位である
`BasicObject`の`method_missing`メソッドが呼ばれ`NoMethodError`を例外として発生させます。

そのため、`BasicObject`までの継承ツリーの途中で`method_midding`メソッドがあれば
それを呼び出します。

```.rb
module HelloModule
  def hello
    puts "Hello from FirstModule"
  end
end

class Klass
  def hello
    puts "Hello from Klass"
  end

  def method_missing(method_name, *args)
    puts "#{method_name}?そんなメソッドないよ"
  end
end

class SubKlass < Klass
  include HelloModule
end

obj = SubKlass.new
obj.bye
# => bye?そんなメソッドないよ
```

## まとめ
探索の流れとして
1 , レシーバの特異クラス
2 , 直接のクラス 
2', moduleがincludeされていればmodule
3 , 2の親クラス
3', moduleがincludeされていればmodule
4 , `3`をメソッドが見つかるまで繰り返す
5 , 継承ツリーの最上位(`BasicObject`)まで探索しなければ、そのメソッドでの探索を終了
6 , メソッド名を引数にして、`method_missing`メソッドを`1`から探していく
7 , なければ`BasicObject`の`method_missing`が呼ばれ`NoMethodError`の例外が発生

# おわり