[Ruby] method_missing()を実用レベルで理解する



## 概要

[メタプログラミングRuby](http://www.amazon.co.jp/gp/product/4048687158/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4048687158&linkCode=as2&tag=kidachi-22)勉強録。

前回は
[Ruby] メタプログラミングの入り口、オープンクラスを理解する
http://qiita.com/kidachi_/items/b1672f1c16e2d15f2d9c


今回のトピックは「Rubyらしいプログラミング」に欠かせない、method_missing()です。

- method_missing()とは
- 何に役立つか
- 危険性と対策


## method_missing()とは

メソッド呼び出しの際、継承チェーン（※）を辿った末に呼び出しメソッドが見つからなかった時、
最終的に呼び出されるメソッド。

※継承チェーンはModule#ancestorsで確認できます。
http://ref.xaio.jp/ruby/classes/module/ancestors


```ruby
class Ruby
end

r = Ruby.new
r.hello
=> NoMethodError: undefined method `hello' for #<Ruby:0x007fe4bb9ac7a0>
```

全てのオブジェクトの継承元であるBasicObjectに定義されているため、
あらゆるオブジェクトからの呼び出しに対応できます。


そして、あくまでこいつも<strong>メソッド</strong>なので、明示的に呼び出すことも可能です。

```ruby
r.send :method_missing, :hello
=> NoMethodError: undefined method `hello' for #<Ruby:0x007fe4bb9ac7a0>
```

※Object#sendについてはこちら。
Ruby send メソッド
http://blog.livedoor.jp/badrequest400/archives/2350825.html

method_missing()自体はこれだけですが、
ここでメタプログラマは考えます。

「逆に言えば、method_missing()のタイミングでうまく拾い上げることで、
<strong>未定義メソッドに対しても好きな処理を加えられる</strong>ってことだよね。」

```ruby
class Ruby
  def method_missing(method, *args)
    # 好きな処理
    puts "#{method}が呼ばれました。"
  end
end
r = Ruby.new
r.hello
=> 'helloが呼ばれました。'
```

こういうことですね。
'NoMethodError'という、（普通にプログラムを書いている限り）絶対の法則を
ねじ曲げることが出来ました。


## 何に役立つか

例えば、外部APIに依存するライブラリ。

サンプルとして、仮にqiitaに関する情報を取得できるAPIがあったとしましょう。
（適当です&実在しませんのであしからず）

```ruby
https://qiita_api.example.com/services/rest/?method=qiita.user.findByName&user_name=kidachi_&api_key=API_KEY
=> 
{
	"result": {
		"user": {
			"id": 8790,
			"name": "kidachi_",
			"profile_image_url": "http://img.example.com/kidachi_.png"
		}
	}
}
```


そして、これをRubyで手軽に扱えるようにサポートするgem（ライブラリ）があるとします。
その名もrubiita（同じく実在しません。ネーミングx）。

```ruby
require 'rubiita'

qiita = Rubiita.new(API_KEY)
kidachi_ = qiita.user_findByName(user_name: "kidachi_")
kidachi_.result.user.profile_image_url
=> "http://img.example.com/kidachi_.png"

qiita_users = qiita.user_findAll
qiita_users.result.users
=>
[
    {
      "id": 1,
      "name": "foo",
      "profile_image_url": "http://img.example.com/foo.png"
    },
    {
      "id": 2,
      "name": "bar",
      "profile_image_url": "http://img.example.com/bar.png"
    }	
]
```

こんな感じですね。
API上での

```sh
?method=qiita.user.findByName&user_name=kidachi_」
```

というパラメータを

```ruby
qiita.user_findByName(user_name: "kidachi_")
```

とRubyライクなメソッドとして扱えるようにしています。

さて、もしかするとここで疑問が出るかもしれません。



「このライブラリ、qiitaのAPIがアップデートされて
使えるメソッドが追加・修正される度にあわせて更新しないと
いけないよね？めんどくさいね」と。

##### 実は、その必要はありません。

rubiitaの実装を見てみましょう。

```ruby
class Rubiita
  require 'net/http'
  require 'uri'
  require 'json'

  END_POINT = 'https://qiita_api.example.com/services/rest/'
  API_KEY = 'hoge'

  def request(method, params)
    json = Net::HTTP.get(get_uri(method, params))
    result = JSON.parse(json)
    puts result
  end

  def method_missing(method, *params)
    request(method.gsub(/_/, '.'), params)
  end

  private
  def get_uri(method, params)
    formatted_params = params.map do |arg|
      param.map do |key, val|
        key.to_s+"="+val.to_s
      end
    end
    formatted_params = formatted_params.join("&")
    method = "?method="+method+"&"
    request_uri = END_POINT+method+formatted_params+API_KEY
  end

end
```

request()はAPIリクエストを飛ばしているだけなのであまり大事ではないです。
ポイントはmethod_missing()とget_uri()ですね。

method_missing()により、
rubiitaから受け取ったメソッド名のアンダースコアをドットに置換し、
パラメータ群はget_uri()に送ります。
そしてget_uri()でGET形式のAPIに適したURIを動的に生成しています。

これにより、qiitaAPIのメソッドが

```sh
?method=qiita.user.findByName&user_name=kidachi_
?method=qiita.user.findAll
?method=qiita.user.newMethodFugaFuga&foo=bar
```

こういったどのようなパターンであっても、
rubiitaユーザに

```ruby
qiita.SEGMENT.METHOD_NAME(PARAMS)
```

というルールに則ってもらいさえすれば、

```ruby
qiita.user_findByName(user_name: "kidachi_")
qiita.user_findAll
qiita.new_methodFugaFuga(foo: bar)
```

全てが適切なメソッドとしてサポートされます。

将来、今は存在しない新しいメソッドが提供されようと、
全て吸収できてしまうのです。

便利ですね！


### ゴーストメソッド

上記例のような、<strong>未定義だがmethod_missing()で拾うことを前提</strong>
としたメソッドを<strong>ゴーストメソッド</strong>と呼びます。


## method_missing()で陥りやすいバグ

最後に、method_missing()の危険性も見ておきます。
※メタプログラミングRubyの例そのままですが。

ぜひ一度どこに問題が潜んでいるか考えてみてください。


```ruby
class Roulette
  def method_missing(name, *args)
    person = name.to_s.capitalize
    3.times do
      number = rand(10) + 1
      puts "#{number}..."
    end
    "#{person} got a #{number}"
  end
end

number_of = Roulette.new
puts number_of.bob
puts number_of.frank
```

このコード、Rouletteの名前通り、以下のように
人物ごとにランダム値が得られる結果を期待しています。

```ruby
5…
6…
10…
Bob got a 10
7…
4…
3…
Frank got a 3
```

ですが、実際には

```ruby
9...
3...
5...
7...
…
…
SystemStackError: stack level too deep from..
```

このように無限ループの後StackOverFlowが発生してします。

原因は、ブロックローカル変数であるnumberです。

- ブロック内で定義したブロックローカル変数は外部から参照できない
- そのため、「"#{person} got a #{number}"」のタイミングで
Rubyはそれ（number）を「メソッド」だと認識し、探しにいく
- しかし継承チェーン内に見つからないためmethod_missing()が呼ばれる
- だが、そもそも今回の問題は（オーバーライドした）method_missing()自体で起きている
- よってバグは無限ループし続ける


この問題、予期できましたでしょうか？

#### method_missingにバグが潜んでいると、特定しづらい（問題に発展しやすい）

ということが分かると思います。

### 対策

シンプルですが、
##### 必要のないゴーストメソッドは導入しない

こと。

つまり、事前に予期されるゴーストメソッド（の名前）以外は受け付けないようにする、ということですね。

##### 対策例

```ruby
class Roulette
  def method_missing(name, *args)
    person = name.to_s.capitalize
    # 想定される3名以外は通常通りのmethod_missing（super）を呼び出す
    super unless %w[Bob Frank Bill].include? person    
    # numberをブロック外から参照できるように事前にセット。
    number = 0
    3.times do
      number = rand(10) + 1
      puts "#{number}..."
    end
    "#{person} got a #{number}"
  end
end

number_of = Roulette.new

# 受け付けられない
puts number_of.hogehoge
=> NoMethodError: undefined method `hogehoge' for #<Roulette:0x007fe4bb9ac7a0>

puts number_of.bob
=> 3...
   5...
   5...
   Bob got a 5
```

## 結論

オープンクラスのエントリの結論と一緒です！
http://qiita.com/kidachi_/items/b1672f1c16e2d15f2d9c#ps1