Chapter 3: Methods - Part II
” Dynamic method is the technique of defining a method at runtime “― Metaprogramming Ruby ―
Defining Methods Dynamically
可先從 Ruby documentation 找到 define_method 的用法:
define_method(symbol, method)
define_method(symbol) { block }
以書中的範例來說:define_method 方法只需要提供
- 方法名稱
- block 裡放要執行程式碼就足夠了。
直接來看一段程式碼:
class MyClass
define_method :my_method do |my_arg|
my_arg * 3
end
end
obj = MyClass.new
obj.my_method(2) # => 6
注意 define_method 是在 MyClass 類別內執行的,所以 my_method() 定義為 MyClass 的實例方法。 這種在 runtime 中定義的方法就被稱為是 Dynamic Method。
Def…end VS. Define_method
嚴格說來,define_method 與我們所熟悉的 def…end 的差別其實不大,最重要的不同是 define_method 允許在 runtime 的環境下,讓你決定方法的名稱。
從以下例子看到 define_method 的好用之處在哪。
class Store
def trade_gold(arg, weight)
"Taipei branch has traded #{arg} for #{weight} kg today"
end
def trade_silver(arg, weight)
"Taipei branch has traded #{arg} for #{weight} kg today"
end
def trade_platinum(arg, weight)
"Taipei branch has traded #{arg} for #{weight} kg today"
end
end
tw_branch = Store.new
tw_branch.trade_gold('gold', 10)
# => "Taipei branch has traded gold for 10 kg so far today"
tw_branch.trade_silver('silver', 22)
# => "Taipei branch has traded silver for 22 kg so far today"
tw_branch.trade_platinum('platinum', 5)
# => "Taipei branch has traded platinum for 5 kg so far today"
有注意到 Store 類別裡的三個方法都蠻相似的,且回傳的內容重複性也蠻高的。 雖然我們也可用傳統的 def…end 來編碼,但要真正地精簡程式碼,可把方法的名稱在同ㄧ個陣列裡,再以 『動態方法』來定義:
class Store
["gold", "silver", "platinum"].each do |metal|
define_method("trade_#{metal}") do |arg|
"Taipei branch has traded #{metal} for #{arg} kg so far today
end
end
end
tw_branch = Store.new
tw_branch.trade_gold(10)
# => "Taipei branch has traded gold for 10 kg so far today"
tw_branch.trade_silver(22)
# => "Taipei branch has traded silver for 22 kg so far today"
tw_branch.trade_platinum(5)
# => "Taipei branch has traded platinum for 5 kg so far today"
可以看出我們並沒有改變回傳值,而是將方法名稱放進陣列,不但減少了重複的程式碼,也讓陣列裡的名稱及對應的方法更有彈性。
如此一來,當需要新增、刪除或是修改方法時,只要控制陣列內的元素即可。
Dynamic Methods
“Where you learn how to call and define methods dynamically, and you remove and duplicated code”― 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.send(:refresh, 10) # => "refreshing in 10 second"
從 Ruby Documentation 可以看到 send() 方法的第一個引數是方法的名稱,而且此名稱必須是以符號(Symbol) 或是 字串(String)來表示,其他需要的引數也都可傳遞。
Dynamic Dispatch
或許你會問既然用逗點+方法名稱或是send()都可以有一樣的效果,為何 Ruby 還要提供 send() 呢?
最大的不同是當使用 send()時,方法名稱就變成為一般的引數而已,因此可以等到條件判斷的結果出來後,再選擇是否要呼叫方法。這樣編碼的技巧就稱為: 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”― 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 )回傳屬性的初始值。
Game.time = 60 # 初始值為 60
我們需要有ㄧ個 refresh() 方法幫助我們重新整理遊戲的設定值。這個 refresh() 方法會以 hash 當引數 (key 是屬性的名稱,value 是屬性的值)。
game.refresh(:time => 60, :score => 0)
game.time # => 60
game.score # => 0
Game#refresh 方法會需要跑過每一個屬性(例如:self.time),並且初始化屬性的設定值(例如: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|
# 第一次的send()
defaults[attribute] = self.send(attribute)
end
defaults.merge!(options).each do |key, value|
# 第二次的send()
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 都會被忽略掉。
- Standard Library API: merge!
Comments