酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

メタプログラミング Ruby 第2版 / 第3章 メソッド[勉強メモ]

メタプログラミングRubyを読んでいます。完全自分用のメモ記事です。 第3章で特に参考になった部分を中心に書いています。


3章 メソッド

動的言語Rubyと静的言語

  • JavaとかCはコンパイラがメソッドのチェックを行う
  • Rubyは実行する際にメソッドを探す。無ければエラーになる

Rubyの動的言語の特徴を活かす

Rubyの動的言語の特徴を利用して、Ruby は重複を排除するために、以下の様な方法を取ることができる。

- 動的にメソッドを定義
- method_missingを使う

動的メソッドを使ったリファクタリング

まずは修正前の変更。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

動的メソッドを使ってリファクタリングを行った結果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
    data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 }
  end

  def self.define_component(name)
    define_method(name) do
      info = @data_source.get_keyboard_info(@id)
      price = @data_source.get_keyboard_price(@id)
      result = "keyboard: #{info} ($#{price})"
      price >= 100 ? "* #{result}" : result
    end
  end
end

method_missing を使った リファクタリング

さきほどの Computer クラスを method_missing でリファクタリングした場合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  def method_missing(name)
    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})"
    price >= 100 ? "* #{result}" : result
  end

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

ただし、 method_missing はバグが発生した場合に気づきにくい特徴があるので利用には注意が必要。 あと、method_missing よりも、respond_to_missing? のほうが適切というお話もあります。

Always Define respond_to_missing? When Overriding method_missing

ゴーストメソッドの最大の問題点は、本物のメソッドではないという点だ。 振る舞いはメソッドなのに、 Object#methods に含まれないのだ。

Pryのコード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def refresh(options={})
  defaults   = {}
  attributes = [ :input, :output, :commands, :print, :quiet,
                 :exception_handler, :hooks, :custom_completions,
                 :prompt, :memory_size, :extra_sticky_locals ]

  attributes.each do |attribute|
    defaults[attribute] = Pry.send attribute
  end

  # ...

  defaults.merge!(options).each do |key, value|
    send("#{key}=", value) if respond_to?("#{key}=")
  end

  true
end

Excerpt From: Paolo Perrotta. “メタプログラミングRuby 2. iBooks.

サンプルソース

何かの役に立つこともあるかもなので、リポジトリも公開しておきます。

morizyun/meta_programming_ruby2 - GitHub


おすすめの書籍