動的ディスパッチ/メソッドの使い方

動的なメソッド生成は同じような役割を持つメソッドを少しづつ役割を変えて、動的に生成することが出来る。

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})"
      return result if price >= 100
      result
    end
  end

  define_component :mouse
  define_component :cpu 
  define_component :keybord
 
end

「define_component」がメソッドの動的な生成を担っている。このメソッドにはクラス定義内でselfが使われており、レシーバーはクラス自体である。つまり、「new」などと同じくクラスメソッドとして生成されている。

これはComputerインスタンスが生成される前に、事前にクラスに「mouse, cpu, keybord」メソッドを定義しておく必要があるからだ。

irb(main):010:0> Computer.methods(false)
=> [:define_component]
irb(main):011:0> Computer.instance_methods(false)
=> [:mouse, :keybord, :cpu]

define_componentメソッドがクラスメソッドであることで、クラス自身に新たなメソッドを追記し、オブジェクトが3つのメソッドを備えることが出来るようになる。

Rubyでは原則メソッドはクラスに存在するものである。一方インスタンス変数はインスタンスに紐づいている。通常「クラスはインスタンスの設計図」であるが、Rubyでは各インスタンスは自分のクラスへのリンクを持っていて、そこに対してメソッドを要求している。)

更に「イントロスペクション」という手法を用いて、DSクラスの構造(定義されているメソッド)を読み込んだうえで、Computerクラスに定義するメソッド名を変化させることもできる。

def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
    data_source.methods.grep(/^get_(.*)_info/){Computer.define_component $1}
    p data_source.methods.grep(/^get_(.*)_info/)
end

これでDSクラスに新しいメソッドが追加されても、Computerクラスはその修正を自動でサポートできる。

動的にメソッドを定義する方法のほかに、メソッドを定義せずに「メソッドに応答があったメソッドだけに答える。」という方法があり、これが「method_missing」を用いる方法である。

def method_missing(name)
    super unless @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

superの節で、そのメソッドがDSクラスに反応するかどうかを確認している。

ただし、この方法だとRubyの組み込みメソッド「respond_to?」をComputerクラスで実行するとfalseが返ってきてしまう。

irb(main):006:0> computer.respond_to?(:cpu)
=> false

それもそのはずで、Computerクラスにはcpuメソッドは定義されていない。DSクラスにget_cpu_infoという形で呼びかけて初めて反応している。

ので、このメソッドもオーバーライドして、DSクラスへ呼びかけて、反応の有無を確かめるべきだ。respond_to?というメソッド自体は respond_to_missing?というメソッドを呼び出しているだけなので、こちらを編集することになる。

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

これで、respond_toが正常に作動するようになった。

irb(main):004:0> cp.respond_to?(:cpu)
=> true

method_missingの編集は見つけにくいバグを発生させやすい。

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がブロック内で定義されているにも関わらず、ブロックの外で呼び出され、当然見つからないので「method_missing」メソッドが再び呼び出されている。

注: Ruby Silverの勉強でやったように、Ruby1.8より前では、ブロックで新しく定義した変数はブロック以降、ずっと有効である。

実は、Rubyの1.9からブロック内の変数のスコープがブロックの中だけになったのであり、Rubyの1.8より前をつかうと「Enjoy2」になるようだ。

Ruby silver模擬試験誤答一覧 その2 - Pyons Tech Blog

この問題に対する対処療法的修正方法は「number」のスコープを大きくしてやることである。しかし、method_missingという、あらゆる場所で使われうるメソッドの挙動を変えるのは極力避けるべきである。method_missingの挙動を変えたい場合に条件を設けてそれ以外は「super」 を呼ぶべきだ。

def method_missing(name, *args)
    person = name.to_s.capitalize
    super unless %w[Yusuke, Shiori, Daisuke]
    number = 0

これで正常に作動する

irb(main):004:0> r.yusuke
6...
9...
8...
=> "Yusuke got a 8"
irb(main):005:0> r.daisuke
5...
9...
4...
=> "Daisuke got a 4"
irb(main):006:0> r.shiori
8...
1...
4...
=> "Shiori got a 4"

mesod_missingを利用して、プログラムを組むとmethod_missingが呼び出されると期待したのに、Objectクラスや中間のモジュールで定義された大量の組み込みメソッドが呼び出されてしまい、期待通りの働きをしないかもしれない。

実際、DSクラスに以下のメソッドを使いしたとしても

  def get_display_info(workstation_id)
    return "large 5.5 inch display"
  end

  def get_display_price(workstation_id)
    return 500
  end

Computerクラスに対して以下のメソッドが実行できない。

irb(main):005:0> pc.mouse
=> "Mouse: 1 has Wireless Touch  ($60)"
irb(main):006:0> pc.display
#<Computer:0x00007fffde936b20>=> nil

これはRubymethod_missingに到達する前に、組み込みクラスObjectクラスにdisplayメソッドが存在するためである。

irb(main):008:0> Object.methods.grep(/display/)
=> [:display]

そこで、「必要最小限のメソッドしか持たないクラス」を実現するために、継承関係の中間に定義されているクラスをすべてすっ飛ばして、直接継承関係の最上部であるBasicObjectから引き継ぐことが出来る。これを「ブランクスレート」と呼ぶ。

ただし、Objectクラスを継承関係から外すとresapnd_to?メソッドが使えなくなる。(@data_source.respond_toはDSクラスに対するメソッド呼び出しなので、そのまま使える。) よって、 Computer.respnd_to(:cpu)も受け付けなくなるので、respond_to_missingをオーバーライドしたモンキーパッチは消してしまうことが出来る。

irb(main):003:0> pc = Computer.new(2,DS.new)
(Object doesn't support #inspect)
=>
irb(main):004:0> pc.cpu
=> "Cpu: 2 has 2.9 Ghz qurd-core  ($120)"
irb(main):005:0> pc.display
=> "Display: large 5.5 inch display  ($500)"
irb(main):006:0> pc.mouse
=> "Mouse: 2 has Wireless Touch  ($60)"

この修正によって、Computerクラスの「inspect」メソッドが死んでしまったという副作用があったが、displayメソッドは期待通り作動した。