雑なメモ

学びを記す

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#ichiroStudentReport#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?に嘘をつかれないようにオーバーライドする必要がある。
また、メソッド名の衝突を避けるためにブランクスレートをする必要もある。

参考書籍