Rubyのメソッドを操作し、コードの重複を排除する
概要
この記事は、メタプログラミングRuby 第2版の第3章「メソッド」を読み学習した内容を個人学習用にまとめ直したものである。
本記事では、Rubyのメソッドの挙動を操り、コードの重複をリファクタリングする様々な手法について解説しています。
動的メソッド
動的ディスパッチ
sendメソッドを使うと、メソッド名を文字列やシンボルで指定して呼び出せる。
第2引数以降は、そのままメソッドの引数として渡される。
class Calculator
def add(a, b)
a + b
end
def greet(name, message: "こんにちは")
"#{message}、#{name}さん"
end
end
calc = Calculator.new
calc.send(:add, 10, 5) # => 15
calc.send(:greet, "太郎", message: "おはよう") # => "おはよう、太郎さん"
# メソッド名を動的に指定
method_name = "add"
calc.send(method_name, 3, 7) # => 10
**通常のドット記法でなくsendを使用すると、呼び出したいメソッドが通常の引数となり、コードの実行時に呼び出すメソッドを決定することができる(動的ディスパッチ)。
**
動的ディスパッチの例:
def refresh(options={})
defaults = {}
attributes = [ :input, :output, :commands, :print, :quiet, :exception_handler, :hooks, :custom_completions, :prompt, :memory_size, :extra_sticky_locals]
attributes.each do |attribute|
defaults[attribute] = Pry.send attribute # 複数のメソッド呼び出しを一行で
end
end
動的メソッド
define_methodを使うと、実行時にメソッドを動的に定義できる(動的メソッド)。
class MyClass
define_method :my_method do |my_arg| # メソッド名とブロックを渡す
my_arg * 3
end
end
obj = MyClass.new
obj.my_method(2) # => 6
動的メソッドにより、実行時にメソッド名を決定することができる。
class Computer
def initialize(computer_id, data_source)
@id = computer_id
# DSモデルのオブジェクト。get_#{name}_info or priceでデータを取得
@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})"
return "* #{result}" if price >= 100
result
end
end
# 実行時にメソッド名が決定され、メソッドが定義される
define_component :mouse
define_component :cpu
define_component :keyboard
end
method_missing
オブジェクトに存在しないメソッドを呼び出すと、継承チェーンを辿って探索が全て終了した後に、全てのオブジェクトが継承するBasicObjectのprivateメソッドであるmethod_missingが呼ばれ、NoMethodErrorが出力される。
nick.my_nonexistent_method
# => NoMethodError
ゴーストメソッド
method_missingをオーバーライドすれば、存在しないメソッドを呼び出した時の挙動を制御することができる。
さらに、**method_missingの中で存在しないメソッドを、呼び出し側からは通常のメソッド呼び出しのように見せる処理をすることも可能となる(ゴーストメソッド)。
**
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
# 継承元のBasicObjectのmethod_missingをオーバーライド
def method_missing(name, *args)
# respond_toがtrueの時、挙動を書き換える。falseの時はError
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
end
my_computer = Computer.new(42, DS.new)
# クラスに定義していないcpuメソッドが通常通り結果を返す
my_computer.cpu # => * Cpu: 2.9 Ghz quad-core ($120)
respond_to_missing?
上記の状態だと、例えばcpuメソッドを呼び出すと通常通り結果が返ってくるが、respond_to?メソッドの返却はfalseのままとなる。
cmp = Computer.new(0, DS.new)
cmp.respond_to?(:cpu) # => false
これはrespond_to?メソッドが、メソッドがゴーストメソッドであった時にtrueを返却するrespond_to_missing?というメソッドを呼び出しているため。
つまり、respond_to?の結果を、ゴーストメソッドを設定した際の実際の挙動と一致させるために,method_missingをオーバーライドした際にはrespond_to_missing?もオーバーライドする必要がある。
class Computer
#...
def respond_to_missing?(method, include_private = false)
@data_source.respond_to?("get_#{method}_info") || super
end
end
cmp.respond_to?(:cpu) # => true
ゴーストメソッドのバグは発見しづらい
以下のように、method_missingの中でバグを発生させてしまうと、本来出力されるべきNoMethodErrorが出力されないのでバグの原因に気付きづらくなる。
class Roulette
def method_missing(name, *args)
person = name.to_s.capitalize
3.times do
number = rand(10) + 1
puts "#{number}..."
end
# timesブロックの中で定義しているnumberを呼び出している
# numberはselfのないメソッド呼び出しと判断され、何度もmethod_missingが呼ばれる
"#{person} got a #{number}"
end
end
number_of = Roulette.new
puts number_of.bob
# => 2...
# => 7...
# => 1...
# => 5...
# ...
# => SystemStackError
このようなバグを防ぐために、大前提として、必要のないゴーストメソッドは導入しないこと。
ブランクスレート
ゴーストメソッドを使う際、継承した既存メソッドと名前が衝突することがある。
class Tag
def method_missing(name, *args)
name.to_s # タグ名を返したい
end
end
Tag.new.display # => "#<Tag:0x00007f>" ← Object#displayが呼ばれてしまう
このような衝突を防ぐために、継承メソッドを最小限に削ぎ落としたクラスをブランクスレートと呼ぶ。
BasicObjectを継承する
BasicObjectはRubyの最上位クラスで、メソッドがほとんど定義されていない。これを継承するのが最もシンプルなブランクスレートの作り方である。
class Tag < BasicObject
def method_missing(name, *args)
name.to_s
end
end
Tag.new.display # => "display" ← ゴーストメソッドが正しく呼ばれる
undef_methodで個別に削除する
BasicObjectを継承せず、特定のメソッドだけを削除したい場合はundef_methodを使う。
class Tag
undef_method :display # Object#displayを削除
def method_missing(name, *args)
name.to_s
end
end
Tag.new.display # => "display" ← ゴーストメソッドが正しく呼ばれる
undef_methodとremove_methodの違い
-
undef_method: メソッドを完全に未定義にする(スーパークラスのメソッドも呼ばれなくなる) -
remove_method: そのクラスのメソッド定義のみ削除する(スーパークラスのメソッドは呼べる)
ブランクスレートでは継承メソッドも含めて削除したいため、undef_methodを使う。
まとめ
-
sendで動的にメソッドを呼び出せる(動的ディスパッチ) -
define_methodで動的にメソッドを定義できる(動的メソッド) -
method_missingをオーバーライドするとゴーストメソッドを実装できる -
respond_to_missing?はmethod_missingとセットで定義する - ゴーストメソッドの名前衝突を防ぐにはブランクスレートを使う
動的メソッドを使うか、ゴーストメソッドを使うかの目安:
「可能であれば動的メソッドを使い、仕方なければゴーストメソッドを使う。」
参考文献
この記事は以下の情報を参考にして執筆した。