動的メソッド
メソッド名、引数値を指定して、メソッドを呼び出す。 これで重複コードをリファクタしたりする。
class MyClass def my_method(my_arg) my_arg * 2 end end obj = MyClass.new() # 普通のメソッド呼び出し obj.my_method(2) => 4 #動的メソッド呼び出し obj.send(:my_method, 2) => 4 #動的メソッド呼び出し(存在しないメソッド名を読んだ場合) obj.send(:my_method1, 2) NoMethodError: undefined method `my_method1' for
send(メソッド名,引数)の形式でメソッドを呼び出せる。 javaのリフレクションと比べるとめちゃ簡単に使える。 いくつもオブジェクト作ったり、例外捕捉したりがいらない。。お手軽。
オブジェクトのプライベートメソッドを覗いてみよう
class MyClass private def my_private_method puts "private method" end end obj = MyClass.new() #通常のレシーバーを用いての呼び出しはできない obj.myprivate_method NoMethodError: undefined method myprivate_method # sendを使った動的呼び出しだと。見える。わぁお。 obj.send(:my_private_method) #=> private method
メソッドを動的に定義する
class MyClass define_method :my_method do |my_arg| my_arg * 3 end end obj = MyClass.new() obj.my_method(3) => 9
Computerのリファクタリング
重複した内容が含まれるクラスを動的ディスパッチャを追加することで改善する。
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 end
内部処理をメソッド化
内部のメソッド呼び出しを抽象化することでメソッド化を可能にしている
def mouse component :mouse end def cpu component :cpu end def component(name) # Object#sendを用いて動的メソッド呼び出しでメソッド呼び出しを抽象化 info = @datasource.send "get_#{name}_info", @id price = @datasource.send "get_#{name}_price", @id # #{}内でも関数が使える。#{}は変数やメソッドを解釈できる result = "#{name.to_s.capitalize} : #{info} ($#{price)}" return "* #{result}" if price >= 100 result end
個人的に気になったのはシンボルを文字列に埋め込むと文字列に自動変換されている箇所
一応試してみたが、問題なく変換されている
name = :AAA => :AAA puts "get_#{name}_info" get_AAA_info
メソッド生成も動的生成にする
Module#define_methodを用いて与えられた名前のメソッドを定義
# クラス内で呼び出すためにクラスメソッドとして定義 def self.define_component(name) define_method(name) { info = @datasource.send "get_#{name}_info" , @id ・・・ } end difine_component :mouse difine_component :cpu
コードイントロスペクションを用いて、メソッド定義を排除。
class Computer def initialize(computer_id, data_source) @id = computer_id @data_source = data_source data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 } def self.define_component(name) { ・・・
grepでマッチした(.*)の部分$1 を用いてブロック内の処理を行うことで メソッド定義が更にシンプル化。。うーむ。すごい。
ちなみにdata_source.methodsの部分がコードイントロスペクション。 オブジェクト内の情報(今回はメソッド)を参照することをコードイントロスペクションと呼ぶらしい。
method_missingについて
・BasicObjectのインスタンスメソッド
・method_missingはメソッド呼び出し時に継承チェーンを上がって調査し、Object・Kernelになかったら元レシーバーのmethod_missing()を呼び出し、NoMethodErrorを出力する。
直接、method_missingを呼び出してみる
myClass.send :method_missing, :my_method NoMethodError: undefined method `my_method` for
method_missingをオーバーライドしてみる
実際には存在しないtalk_simpleメソッド(ゴーストメソッド)を呼び出せ、
メソッド名、引数情報、ブロックが存在するかなどの情報を活用できる。
class Lawyer def method_missing(method, *args) puts "#{method}(#{args.join(',')})を呼び出した" puts "(ブロックを渡した)" if block_given? end end bob = Lawyer.new => #<Lawyer:0x007fc4c90c6d50> bob.talk_simple('a','b') do "ブロック" end talk_simple(a,b)を呼び出した (ブロックを渡した)
OpenStructクラスでゴーストメソッドを体験する
require 'ostruct' icecream = OpenStruct.new => #<OpenStruct> # 存在しない、flavor=()メソッドを呼び出せる icecream.flavor = "ストロベリー" # 存在しない、flavorメソッドを呼び出せる icecream.flavor => "ストロベリー"
OpenStructの処理はどうなっているんだろう。
簡単な内部処理をシンプルに書くと以下のようになるそう。
class MyOpnenStruct def initialize @attributes = {} end def method_missing(name, *args) attribute = name.to_s # =〜な名称ならばハッシュに=を削除したKeyで引数に設定された値を設定 if attribute =~ /=$/ @attributes[attribute.chop] = args[0] # =がつかないメソッド名の場合は取得名称をKeyとしてハッシュより値取得 else @attributes[attribute] end end end icecream = MyOpenStruct.new icecream.flavor = "バニラ" icecream.flavor # => "バニラ"
Computerクラスのリファクタリング(method_missing)
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})" #{result}" if price >= 100 result end end
method_missingを用いてリファクタリング
class Computer ・・・ method_missing(name, *args, ) # 呼び出しメソッドが存在確認。存在しない場合、元のmethod_missingの処理を呼ぶ super if !@data_source.respond_to?("get_#{name}_info") # メソッド名を元に動的ディスパッチ(呼び出し)を行う。 info = @data_source.send("get_#{name}_info" ,arg[0]) price = @data_source.send("get_#{name}_price" ,arg[0]) result = "#{id.capitalize}: #{info} ($#{price})" return "* #{result}" if price >= 100 result end com = Computer.new() com.mouse com.cpu
get_mouse_infoなどのゴーストメソッドはrespond_to?しても存在しないと言われる。
これを回避するにはrespond_to?をクラス内で上書きして、 method_missing内で呼ばれるメソッドの存在チェックをするとよい。
# メソッド上書き前 com.respond_to?(:mouse) # => false class Computer def respond_to?(method) @data_source.respond_to?("get_#{method}_info") || super end ・・・ # メソッド上書き後 com.respond_to?(:mouse) # => true
メソッド名の衝突を回避せよ
method_missingで呼び出す予定のメソッドが既存ですでに定義済みの場合がある。
その場合は存在するメソッドが呼ばれてしまう。これを回避するには以下
class Computer instance_methods.each do |m| #パターンに示したメソッド以外のObjectから継承したメソッドを削除 undef_method m unless m.to_s =~ /^__|method_missing| respond_to?/ end
Objectから継承したメソッドをすべて削除したブランクストレートを継承するとメソッド名の衝突を回避できる。上記例は、一部メソッドを残しているので、純粋なブランクストレートではない。
Ruby1.9からブランクストレートが言語に組み込まれた。 クラスルートのBasicObjectである。これは必要最低限のメソッドしかないクラス。
BasicObject.instance_methods => [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]
BasicObjectを直接継承したクラスはブランクストレート(Object内のインスタンスメソッドを継承していないクラス)となる。