method_missingについて学んだ
引き続き、黒魔術を勉強中。まじで楽しい。
method_missingについてまとめる。
意味
例えば...
class Yukiyan; end yukiyan = Yukiyan.new yukiyan.hello => NoMethodError: undefined method `hello' for #<Yukiyan:0x007fc7de4b7550> 〜
これは、yukiyanの中にhelloなんてメソッド見つからないからmethod_missing()を呼び出して例外NoMethodErrorを吐いている。
method_missingはBasicObjectのprivateメソッドだから全てのオブジェクトが持っているメソッドである。
[1] pry(main)> BasicObject.private_methods.grep /^method_m/ => [:method_missing]
ユースケース
今回もリファクタリングしながら理解していく。
コード例は前回のを使い回します。
class StudentData def get_ichiro_info "genius" end def get_ichiro_height 170 end def get_jiro_info "shorter" end def get_jiro_height 140 end def get_saburo_info "cool" end def get_saburo_height 150 end end
データ出力用にこんなクラスがある。
$:.unshift File.dirname(__FILE__) require 'student-data' student_data = StudentData.new class StudentReport def initialize(data_source) @data_source = data_source end def ichiro info = @data_source.get_ichiro_info height = @data_source.get_ichiro_height result = "Ichiro is #{info}. height : #{height} cm" result end def jiro info = @data_source.get_jiro_info height = @data_source.get_jiro_height result = "Jiro is #{info}. height : #{height} cm" result end def saburo info = @data_source.get_saburo_info height = @data_source.get_saburo_height result = "Saburo is #{info}. height : #{height} cm" result end # def siro ... # def goro ... # コピペの嵐になってく end
こんな感じに動作する。
student_report = StudentReport.new(student_data) puts student_report.ichiro # => Ichiro is genius. height : 170 cm puts student_report.jiro # => Jiro is shorter. height : 140 cm puts student_report.saburo # => Saburo is cool. height : 150 cm
StudentDataを追加するとStudentReportに似たようなメソッドが増えていく。
この問題をmethod_missingを使って解決する。
リファクタリング第一段階
method_missingをオーバーライドして、存在しないメソッドが呼ばれたときの挙動を変えてしまえばいい。
StudentReport#ichiro
やStudentReport#jiro
は、どれも内部的には、get_***_infoを呼んでるだけである。
なので、method_missingの定義を、「存在しないメソッドが呼ばれたら例外吐くけど、get_***_infoという名前のメソッドについては独自の振舞いをさせる」ようにすればいい。
$:.unshift File.dirname(__FILE__) require 'student-data' student_data = StudentData.new class StudentReport def initialize(data_source) @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") height = @data_source.send("get_#{name}_height") result = "#{name} is #{info}. height : #{height} cm" result end end
respond_to?メソッドは、レシーバのオブジェクトに対してメソッドを呼び出せるかどうかを真偽値で返す。
リファクタリング第二段階
上記のコードの場合だとこんな問題が起きる。
student_report = StudentReport.new(student_data) puts student_report.ichiro # => Ichiro is genius. height : 170 cm puts student_report.respond_to?(:ichiro) # => false
student_report#ichiro()は存在するはずなのにfalseが返されてしまっている。 結果を正当なものにするためにrespond_to?をオーバライドする必要がある。
$:.unshift File.dirname(__FILE__) require 'student-data' student_data = StudentData.new class StudentReport def initialize(data_source) @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") height = @data_source.send("get_#{name}_height") result = "#{name} is #{info}. height : #{height} cm" result end def respond_to?(method) @data_source.respond_to?("get_#{method}_info") || super # デフォルトのrespond_to?の動作も保証したいのでsuperを呼び出してる。 end end
リファクタリング第三段階
実はこれでもまだ不十分。
動的にメソッドを定義する際に、偶然既存のメソッドと同名のメソッドを定義してしまうことがある。
全てのオブジェクトはObjectやBasicObjectを継承しているので、現段階だと、継承元に定義されてるメソッド名と衝突する可能性がある。
メソッド名の衝突による障害を防ぐために、student_reportのインスタンスメソッドをあらかじめ全て削除しておく必要がある。
$:.unshift File.dirname(__FILE__) require 'student-data' student_data = StudentData.new class StudentReport instance_methods.each do |m| undef_method m unless m.to_s =~ /^__|method_missing|respond_to?|object_id/ end def initialize(data_source) @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") height = @data_source.send("get_#{name}_height") result = "#{name} is #{info}. height : #{height} cm" result end def respond_to?(method) @data_source.respond_to?("get_#{method}_info") || super end end
この場合は、method_missingとrespond_to?は使うので削除しない。
また、__で始まるメソッドとobject_idというメソッドを削除するとwarningが出てしまうので、これも削除しないようにする。ちなみに、このようにメソッドをほとんど空っぽの状態にすることをブランクスレートという。
Ruby1.9以上なら...
ブランクスレートの部分について、Ruby1.8までなら上記の書き方でいいが、Ruby1.9以上ならもっとスマートにかける。
$:.unshift File.dirname(__FILE__) require 'student-data' student_data = StudentData.new class StudentReport < BasicObject def initialize(data_source) @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") height = @data_source.send("get_#{name}_height") result = "#{name} is #{info}. height : #{height} cm" result end def respond_to?(method) @data_source.respond_to?("get_#{method}_info") || super end end
BasicObjectクラスを継承すれば一発でブランクスレートできる。 BasicObjectクラスは Object クラスからほとんどのメソッドを取り除いたクラスで、Objectの親クラス。
まとめ
method_missingは、動的にメソッドを定義したいときに便利。
でも、Object#respond_to?に嘘をつかれないようにオーバーライドする必要がある。
また、メソッド名の衝突を避けるためにブランクスレートをする必要もある。