動的ディスパッチと動的メソッドについて学んだ
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
StudentReport.new(student_data)
されることでstudent_data.methods.grep(/^get_(.*)_info$/)
される。
つまり、StudentDataのメソッドから、get_***_info
というパターンのメソッドがgrepされる。- grepされた要素全てに対して
StudentReport.define_student
が評価される。 - 正規表現の
(.*)
でマッチした文字列("ichiro"とか"jiro")は全てグローバル変数$1に格納されるので、define_method("ichiro")みたいな感じで、$1の要素と同名のメソッドが定義されてく。
まとめ
動的ディスパッチと動的メソッドを使えば、コードの重複をがんがん無くせる。
イントロスペクションを使えば、更に改善できることもある。
正規表現を使うと便利。