読者です 読者をやめる 読者になる 読者になる

雑なメモ

学びを記す

動的ディスパッチと動的メソッドについて学んだ

Ruby

Rubyの黒魔術を勉強中。
動的メソッドと動的ディスパッチについてまとめる。

意味

ユースケース

リファクタリングしながら理解していく。
例えば、こんなひどいコードでデータが格納されてるとする。
(本当はデータソースに接続する処理がinitializeに書かれてたりするけど、説明を簡単にするため、あえてこんな書き方にした。)

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

見て分かる通り、ichiroメソッドもjiroメソッドも中身はほぼコピペに近い。
このままだと、StudentDataにデータ追加されるごとに似たようなメソッドを定義しなければならない。 そんな方法はとりたくない!
そこで動的ディスパッチと動的メソッドを使う!

リファクタリング 第一段階

実は、メソッドの呼び出し方法にはドットで呼ぶ方法以外に、Object#send()を使った呼び出し方法がある。
send()の第一引数はオブジェクトに送信するメッセージ(メソッド名)で、第二引数以降はメソッドに渡される。

$:.unshift File.dirname(__FILE__)
require 'student-data'

student_data = StudentData.new

class StudentReport
  def initialize(data_source)
    @data_source = data_source
  end

  def ichiro
    student :ichiro
  end

  def jiro
    student :jiro
  end

  def saburo
    student :saburo
  end

  def student(name)
    info = @data_source.send "get_#{name}_info"
    height = @data_source.send "get_#{name}_height"
    result = "#{name.to_s.capitalize} is #{info}. height : #{height} cm"
    result
  end

end

こんな感じに動作する。

student_report = StudentReport.new(student_data)
puts student_report.jiro # => Jiro is shorter. height : 140 cm

send()を使うことで、引数としてメソッドを呼び出せるので、コードの実行時に呼び出すメソッドを 動的に決められる。これが動的ディスパッチという魔術である

リファクタリング 第二段階

Module#define_method()でメソッドを動的に定義できる。
シンボルとしてメソッド名を渡し、中身はブロックを渡すことで定義できる。

$:.unshift File.dirname(__FILE__)
require 'student-data'

student_data = StudentData.new

class StudentReport
  def initialize(data_source)
    @data_source = data_source
  end

  def self.define_student(name)
    define_method(name){
      info = @data_source.send "get_#{name}_info"
      height = @data_source.send "get_#{name}_height"
      result = "#{name.to_s.capitalize} is #{info}. height : #{height} cm"
      result
    }
  end

  define_student :ichiro
  define_student :jiro
  define_student :saburo

end

これが動的メソッドだ!

リファクタリング 最終段階

コードの重複をだいぶ減らせたが、まだ改善の余地がある。
イントロスペクションという技術でさらにリファクタリングできる。
Javaでいうリフレクションみたいなやつ。
プログラム実行時にオブジェクトの情報を参照すること。
この例の場合は、data_sourceにはどんなメソッドがあるのか参照してgrepしてる。

$:.unshift File.dirname(__FILE__)
require 'student-data'

student_data = StudentData.new

class StudentReport
  def initialize(data_source)
    @data_source = data_source
    data_source.methods.grep(/^get_(.*)_info$/) { StudentReport.define_student $1 }
        # イントロスペクションを使ってる。
  end

  def self.define_student(name)
    define_method(name){
      info = @data_source.send "get_#{name}_info"
      height = @data_source.send "get_#{name}_height"
      result = "#{name.capitalize} is #{info}. height : #{height} cm"
      result
    }
  end

end

grepのところで、個人的に美しいと感じた魔術が使われている。 順に処理を追ってみる。 これも今までどおりの呼び出し方法で動作する。

student_report = StudentReport.new(student_data)
puts student_report.jiro    # => Jiro is shorter. height : 140 cm
  1. StudentReport.new(student_data)されることでstudent_data.methods.grep(/^get_(.*)_info$/)される。
    つまり、StudentDataのメソッドから、get_***_infoというパターンのメソッドgrepされる。
  2. grepされた要素全てに対してStudentReport.define_studentが評価される。
  3. 正規表現(.*)でマッチした文字列("ichiro"とか"jiro")は全てグローバル変数$1に格納されるので、define_method("ichiro")みたいな感じで、$1の要素と同名のメソッドが定義されてく。

まとめ

動的ディスパッチと動的メソッドを使えば、コードの重複をがんがん無くせる。
イントロスペクションを使えば、更に改善できることもある。
正規表現を使うと便利。

参考書籍