メタプログラミングRuby3章を読んでそれをまとめてみようとおもいます。
タイトルにある動的メソッドとは何なのでしょうか?
動的にメソッドを呼び出す
Rubyではメソッドを呼びだすときはドットを使って呼び出しています。
array.count
のようにオブジェクトにたいしてメソッドを呼び出しています。
そのほかにsendメソッドを使う方法もあります。以下のような使い方をします
class Example
def ex_method(arg)
arg * 3
end
end
obj = Example.new
obj.ex_method(30) #=> 90
obj.send(:ex_method, 30) #=> 90
同じex_methodを呼び出してはいますが、コードを実行する際に呼び出すメソッドを決められるのがsendメソッドのポイントです。
このように、コードの実行時に呼び出すメソッドを決められることを動的ディスパッチといいます。
このようにメソッドを動的に呼び出すことができることを紹介しましたが、メソッドを動的に定義することも可能なんです。
動的にメソッドを定義する
動的にメソッドを定義するために使われるメソッドは、define_method
です。
このメソッドを使えば、実行時にメソッド名を決めれるんです。これを動的メソッドといいます
動的ディスパッチと動的メソッドを使って下のコードをリファクタしてみましょう。
※以下のコードは参考資料に書いてある『メタプログラミングRuby』から引用しているコードです。
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
info =@data_source.get_mouse_info(@id)
price = @data_source.get_mouse_price(@id)
result = "Mouse: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
def cpu
info =@data_source.get_cpu_info(@id)
price = @data_source.get_cpu_price(@id)
result = "Cpu: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
def keyboard
info =@data_source.get_keyboard_info(@id)
price = @data_source.get_keyboard_price(@id)
result = "Keyboard: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
#...
end
上記のコードだとハードウェアが増えれば増えるほど、メソッドが必要になってしまうので、保守性が保てなくなってしまいますね。
この記事で、def
以外にdefine_method
でメソッドを定義する方法を紹介しました。
また、メソッドを呼び出す方法として、ドットを使う以外にsend
メソッドを使ってメソッドを呼び出す方法を紹介しました。
これらを使って、リファクタリングしてみましょう。重複したコードを抽出してメッセージを出力するメソッドにまとめてみます。
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
component :mouse
end
def cpu
component :cpu
end
def keyboard
component :keyboard
end
def component(name)
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
新たにcomponentメソッドを用意してそこで、メッセージを出力するコードを記述している。だいぶスッキリしたような感じになりましたね。
でも、def
キーワードが似たような使われ方をしていますね。そこをリファクタしてみましょう。
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def self.define_computer(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 :keyboard
end
Computerクラスにdefine_componentを呼び出したいのでクラスメソッドを指定しています。
method_missing
method_missingとは継承チェーンをたどってメソッドが見つからなかった際に最後に呼び出されるメソッドです。いうならばエラー文ですね。NomethodErrorが有名ですね。
そしてこのメソッドは、BasicObjectクラスに定義されているので、どこからでも呼び出すことができるんですね。
本書では、method_missingを使ってリファクタもされていますが、このメソッドは使い方を間違うとメンドクサイことになります
例えば以下のコードを見てください
※以下のコードは参考資料に書いてある『メタプログラミングRuby』から引用しているコードです。
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_of = Roulette.new
puts number_of.bob
puts number_of.frank
# => 数字がズラズラ...
...
1...
9...
10...
7...
SystemStackError (stack level too deep)
このようにSystemStackErrorと出てしまい、どこに問題があるかが分からなくなってしまうのがこのメソッドの怖いところです。
これが大規模のシステムになったらメチャクチャめんどくさいことになります。
この章を読んで思ったのは、できるだけmethod_missing
は使わない方がいいと思いました。
できるだけ動的メソッドを使って重複コードを無くした方がいいとおもいました
以上です。何か間違いがございましたら、ご教示いただけますと幸いです。
【参考資料】