動的ディスパッチ/メソッドの使い方
動的なメソッド生成は同じような役割を持つメソッドを少しづつ役割を変えて、動的に生成することが出来る。
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
これはRubyがmethod_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
メソッドは期待通り作動した。