iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 11
0
自我挑戰組

Metaprogramming Ruby and Rails系列 第 11

Day 11 -- Dynamic Methods Part I

Dynamic Methods

"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

" 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 都會被忽略掉。


上一篇
Day 10 -- Singleton class In Ruby 神秘的匿名者 PART II
下一篇
Day 12 -- Dynamic Methods Part II
系列文
Metaprogramming Ruby and Rails33

尚未有邦友留言

立即登入留言