概要
メタプログラミングRuby の第3章 メソッド を読んだ学習内容のメモ。
最初にざっくりまとめ
内容的には、メソッド定義における重複コードの問題について解決策を提示する、というもの。
- 解決方法は大きく2種類のアプローチがある。
- 動的メソッド(
define_method
) - ゴーストメソッド(
method_missing
)
- 動的メソッド(
- 可能な限りは動的メソッドを使い、仕方のない場合はゴーストメソッドを使いましょう。
例題
「99ドルより高いコンピュータの部品を検出するシステムを作る」という、具体的な例題をベースに進んでいきます。
重複コード問題
このようなコードの重複が多いメソッドをどのように改善していくか...
# 重複がたくさんある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
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
動的メソッドを用いた解決方法
動的ディスパッチ
メソッドの呼び出しに、通常使用するドット記法(obj.my_method(arg)
)ではなく、 Object#send
を使う(obj.send(:my_method, arg)
)。
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_souce.send "get_#{name}_price", @id
result = "#{name.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
動的メソッド
Module#define_method
を使うと、メソッドを動的に定義する事ができる。
メソッド名とブロックを渡す必要があり、ブロックがメソッドの本体になる。
ここではComputerクラス定義の中で define_method
を呼び出したいので、クラスメソッドにする必要がある。
# define_methodを用いてさらにリファクタリング
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def self.define_component(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})"
retrun "* #{result}" if price >= 100
result
end
end
define_component :mouse
define_component :cpu
define_component :keyboard
end
するとこんな風に使える
obj = Computer.new(42, data_source)
obj.mouse # => "Wireless Touch"
obj.price # => 60
さらにインストロペクション
さらに重複を排除するため、data_source
をインストロペクション(*)してコンポーネントの名前に展開する。
# data_sourceをインストロペクション!
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
# get_xxx_infoというメソッドのリストにブロックを渡して、
# 正規表現にマッチした文字列(mouse, cpu等)の名前のメソッドを定義する
data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 }
end
def self.define_component(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})"
retrun "* #{result}" if price >= 100
result
end
end
end
これで、data_source
側にコンポーネントが追加されても、Computerクラスをいじる事なくサポートできるようになった。
(*) インストロペクション ... オブジェクトに対して言語要素(変数、クラス、メソッド等)を質問する事
"hoge".class # classを聞いてみる
=> String
"hoge".methods.grep(/to_(.*)/) # "to_"から始まるメソッドを聞いてみる
=> [:to_c, :to_str, :to_sym, :to_s, :to_i, :to_f, :to_r, :to_json_raw, :to_json_raw_object, :to_json, :to_enum]
ゴーストメソッドを用いた解決方法
method_missing
存在しないメソッドを呼び出した時、 BasicObject#method_missing
が呼ばれる。
よくあるこんな感じ。
class Lawyer; end
nick = Lawyer.new
nick.talk
=> NoMethodError: undefined method `talk' for #<Lawyer:0x00007f921c0f2958>
この method_missing
をオーバーライドすることで、実際には存在しないメソッドを呼び出せるようになる。
method_missing
で処理されるがレシーバ側には対応するメソッドがない、このようなメソッドを ゴーストメソッド と呼ぶ。
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def method_missing(name)
# @data_sourceに該当のメソッドがなければ親クラスのmethod_missingを呼ぶ
super if !@data_source.respond_to?("get_#{name}_info")
# メソッドがあれば以下の処理をする
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
def respond_to_missing?(method, include_private = false)
@data_source.respond_to?("get_#{method}_info") || super
end
end
method_missing
のバグは潰しにくい
例)
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
ブランクスレート
method_missing
のもうひとつの罠
先の Computer
クラスでは display
メソッドだけ正常に動かない。
my_computer = Computer.new(42, DS.new)
my_computer.display # => nil
なぜか。
=> 継承元の Object
クラスに display
メソッドが既に定義されているから。
Object.instance_methods.grep /^d/
=> [:define_singleton_method, :display, :dup]
Computer#display
を呼び出しているつもりが、 Object#display
が見つかってしまうため、 method_missing
にたどり着かない。
これを解決するには、不要なメソッドは削除された状態にしておく必要がある。
最小限のメソッドしかない状態のクラスを、 ブランクスレート と呼ぶ。
ブランクスレートの実現方法として、BasicObject
クラスを継承する方法・不要なメソッドを削除する方法が紹介されている。
結論
このように、ゴーストメソッドは便利だが見つけにくいバグを含む危険をはらんでいる。
で、以下の結論。
「可能な限りは動的メソッドを使い、仕方のない場合はゴーストメソッドを使いましょう。」
今日は家に帰って休息だ。