[学習メモ]メタプログラミングRuby第2版：3章：メソッド

# 概要
[メタプログラミングRuby](https://www.oreilly.co.jp/books/9784873117430/) の第3章 メソッド を読んだ学習内容のメモ。

## 最初にざっくりまとめ
内容的には、メソッド定義における重複コードの問題について解決策を提示する、というもの。

- 解決方法は大きく2種類のアプローチがある。
    1. 動的メソッド(`define_method`)
    1. ゴーストメソッド(`method_missing`)
- 可能な限りは動的メソッドを使い、仕方のない場合はゴーストメソッドを使いましょう。

## 例題
「99ドルより高いコンピュータの部品を検出するシステムを作る」という、具体的な例題をベースに進んでいきます。

## 重複コード問題
このようなコードの重複が多いメソッドをどのように改善していくか...

``` ruby
# 重複がたくさんあるComputerクラス
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  def mouse
    info = @data_source.get_mouse_info(@id)
    price = @data_source.get_mouse_price(@id)
    result = "Mouse: #{info} ($#{price})"
    return "* #{result}" if price >= 100
    result
  end

  def cpu
    info = @data_source.get_cpu_info(@id)
    price = @data_source.get_cpu_price(@id)
    result = "Cpu: #{info} ($#{price})"
    return "* #{result}" if price >= 100
    result
  end

  def keyboard
    info = @data_source.get_keyboard_info(@id)
    price = @data_source.get_keyboard_price(@id)
    result = "Keyboard: #{info} ($#{price})"
    return "* #{result}" if price >= 100
    result
  end

  # ....
end
```

## 動的メソッドを用いた解決方法
### 動的ディスパッチ
メソッドの呼び出しに、通常使用するドット記法(`obj.my_method(arg)`)ではなく、 `Object#send` を使う(`obj.send(:my_method, arg)`)。
`send` を使うことによって、呼び出したいメソッド名が引数となり、メソッド名を動的に指定する事ができる。
このような手法を **動的ディスパッチ** と呼ぶ。

``` ruby
# 動的ディスパッチを利用したリファクタリング
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  def mouse
    component :mouse
  end

  def cpu
    component :cpu
  end

  def keyboard
    component :keyboard
  end

  def component(name)
    info = @data_source.send "get_#{name}_info", @id
    price = @data_souce.send "get_#{name}_price", @id
    result = "#{name.capitalize}: #{info} ($#{price})"
    return "* #{result}" if price >= 100
    result
  end
end
```

### 動的メソッド
`Module#define_method` を使うと、メソッドを動的に定義する事ができる。 
メソッド名とブロックを渡す必要があり、ブロックがメソッドの本体になる。
ここではComputerクラス定義の中で `define_method` を呼び出したいので、クラスメソッドにする必要がある。

``` ruby
# define_methodを用いてさらにリファクタリング
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  def self.define_component(name)
    define_method(name) do
      info = @data_source.send "get_#{name}_info", @id
      price = @data_source.send "get_#{name}_price", @id
      result = "#{name.capitalize}: #{info} ($#{price})"
      retrun "* #{result}" if price >= 100
      result
    end
  end

  define_component :mouse
  define_component :cpu
  define_component :keyboard
end
```
するとこんな風に使える

``` ruby
obj = Computer.new(42, data_source)
obj.mouse # => "Wireless Touch"
obj.price # => 60
```

### さらにインストロペクション
さらに重複を排除するため、`data_source` をインストロペクション(*)してコンポーネントの名前に展開する。

``` ruby
# data_sourceをインストロペクション！
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
    # get_xxx_infoというメソッドのリストにブロックを渡して、
    # 正規表現にマッチした文字列(mouse, cpu等)の名前のメソッドを定義する
    data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 }
  end

  def self.define_component(name)
    define_method(name) do
      info = @data_source.send "get_#{name}_info", @id
      price = @data_source.send "get_#{name}_price", @id
      result = "#{name.capitalize}: #{info} ($#{price})"
      retrun "* #{result}" if price >= 100
      result
    end
  end
end
```

これで、`data_source` 側にコンポーネントが追加されても、Computerクラスをいじる事なくサポートできるようになった。

(*) インストロペクション ... オブジェクトに対して言語要素(変数、クラス、メソッド等)を質問する事

``` ruby
"hoge".class # classを聞いてみる
=> String
"hoge".methods.grep(/to_(.*)/) # "to_"から始まるメソッドを聞いてみる
=> [:to_c, :to_str, :to_sym, :to_s, :to_i, :to_f, :to_r, :to_json_raw, :to_json_raw_object, :to_json, :to_enum]
```

## ゴーストメソッドを用いた解決方法
### method_missing
存在しないメソッドを呼び出した時、 `BasicObject#method_missing` が呼ばれる。
よくあるこんな感じ。

``` ruby
class Lawyer; end
nick = Lawyer.new
nick.talk
=> NoMethodError: undefined method `talk' for #<Lawyer:0x00007f921c0f2958>
```

この `method_missing` をオーバーライドすることで、実際には存在しないメソッドを呼び出せるようになる。
`method_missing` で処理されるがレシーバ側には対応するメソッドがない、このようなメソッドを **ゴーストメソッド** と呼ぶ。

``` ruby
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  def method_missing(name)
    # @data_sourceに該当のメソッドがなければ親クラスのmethod_missingを呼ぶ
    super if !@data_source.respond_to?("get_#{name}_info")

    # メソッドがあれば以下の処理をする
    info   = @data_source.__send__("get_#{name}_info", @id)
    price  = @data_source.__send__("get_#{name}_price", @id)
    result = "#{name.capitalize}: #{info} ($#{price})"
    return "* #{result}" if price >= 100
    result
  end

  def respond_to_missing?(method, include_private = false)
    @data_source.respond_to?("get_#{method}_info") || super
  end
end
```

### `method_missing` のバグは潰しにくい
例)

``` 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
```

### ブランクスレート
`method_missing` のもうひとつの罠

先の `Computer` クラスでは `display` メソッドだけ正常に動かない。

``` ruby
my_computer = Computer.new(42, DS.new)
my_computer.display # => nil
```

なぜか。
=> 継承元の `Object` クラスに `display` メソッドが既に定義されているから。

``` ruby
Object.instance_methods.grep /^d/
=> [:define_singleton_method, :display, :dup]
```

`Computer#display` を呼び出しているつもりが、 `Object#display` が見つかってしまうため、 `method_missing` にたどり着かない。

これを解決するには、不要なメソッドは削除された状態にしておく必要がある。
最小限のメソッドしかない状態のクラスを、 **ブランクスレート** と呼ぶ。

ブランクスレートの実現方法として、`BasicObject` クラスを継承する方法・不要なメソッドを削除する方法が紹介されている。

## 結論
このように、ゴーストメソッドは便利だが見つけにくいバグを含む危険をはらんでいる。
で、以下の結論。

 **「可能な限りは動的メソッドを使い、仕方のない場合はゴーストメソッドを使いましょう。」** 

今日は家に帰って休息だ。