"Where you learn how to call and define methods dynamically, and you remove and duplicated code" from Metaprogramming Ruby
ㄧ般常見的呼叫方法是用逗點+方法名稱:
class Game
def refresh(time)
"refreshing in #{time} second"
end
end
game = Game.new
game.refresh(10) # => "refreshing in 10 second"
不過還有另ㄧ種寫法可以呼叫 refresh() 方法: 使用 Object 類別裡的 send() 方法,呼叫 Game#refresh:
game.send(:refresh, 10) # => "refreshing in 10 second"
從 Ruby Documentation 可以看到 send() 方法的第一個引數是方法的名稱,而且此名稱必須是以符號(Symbol) 或是 字串(String)來表示,其他需要的引數也都可傳遞。
" Dynamic dispatch is the technique that let you wait until the very last moment to decide which method to call, while the code is running" from Metaprogramming Ruby
這個部分將以類似書中的例子來說明 dynamic dispatch:我們建立了一個 Game object 並將遊戲設定都存放在屬性裡。
game = Game.new
game.time = 23
game.score = 0
game.rank = 0
game.memory_size = 1800
而每ㄧ個實例方法(例如: Game#time) 都會有對應的類別方法,回傳設定屬性的初始值。
Game.time = 60
我們需要有ㄧ個 refresh() 方法幫助我們重新整理遊戲的設定值。這個 refresh() 方法會以 hash 當引數 (key 是屬性的名稱,value 是屬性的值)。
game.refresh(:time => 60, :score => 0)
game.time # => 60
game.score # => 0
refresh() 方法會需要跑過每一個屬性,並且初始化屬性的設定值(例如:Game.time),最後再檢查 hash 引數內的同樣的屬性是否有新的值,來決定要不要更新屬性。
def refresh(options={})
defaults[:time] = Game.time
self.time = options[:time] if options[:time]
defaults[:score] = Game.score
self.score = options[:score] if options[:score]
.... ㄧ直重複編碼直到完成其餘的屬性
end
不難猜想如果未來屬性數量持續增加的話,此方法不僅有維護上的問題,程式碼區塊也將倍數成長。要解決以上問題,我們可以使用 Dynamic dispatch 技巧,只需要幾行的程式碼就可以設定好所有的值。
class Game
attr_accessor :time, :score, :limit, :rank, :memory_size
def initialize(time=60, score=0, limit=0, rank=0, memory_size=0)
@time = time
@score = score
@limit = limit
@rank = rank
@memory_size = memory_size
end
def refresh(options={})
defaults = {}
attributes = [ :time, :score, :limit, :rank, :memory_size ]
attributes.each do |attribute|
defaults[attribute] = self.send(attribute)
end
defaults.merge!(options).each do |key, value|
send("#{key}=", value) if respond_to?("#{key}=")
end
end
end
game = Game.new
game.refresh({:limit=> 100, :memory_size => 99})
# => {:time=>60, :score=>0, :limit=>100, :rank=>0, :memory_size=>99}
在上面的程式碼內,第一次的 send() 方法是用來將屬性的初始值,放進 defaults[attribute] 裡,接著再與 options hash 合併,最後使用 dynamic dispatch 技巧以 send 方法呼叫 attribute accessors ( 例如:time= )。
注意:respond_to? 方法是用來檢查 Game#time= 方法是否存在?因此在 options hash 只要是沒有跟現有屬性相配的 key 都會被忽略掉。