本篇要分享的是此書(在第三章)我蠻喜歡的範例之一,作者以說故事的方式講解本章節所介紹的題目,假設的情境是新進員工被會計部門賦予一項任務,目標是找出花費大於美金$99元的電腦配件。
我做了此範例可使用的 DS(搭配CSV檔),這樣修改起來比較有感覺。大家如有有興趣可以在我的 GitHub The Legacy System Demo下載。
主角所面臨的舊系統中存在著許多重複及冗長的程式碼,達成任務的過程就是以本章學習到的技巧來重構更簡潔、易維護的系統。
還未修改的程式碼如下:
class DS
def initialize # connect to data source...
def get_cpu_info(workstation_id) # ...
def get_cpu_price(workstation_id) # ...
def get_mouse_info(workstation_id) # ...
def get_mouse_price(workstation_id) # ...
def get_keyboard_info(workstation_id) # ...
def get_keyboard_price(workstation_id) # ...
def get_display_info(workstation_id) # ...
def get_display_price(workstation_id) # ...
# ...and so on
當建立新的 DS 實例物件時,DS#initializ 方法就會連接到資料系統裡。如需要詢問特定電腦的配件資訊,可以將該電腦ID帶入到對應的實例方法中,就可以得到相關訊息。
ds = DS.new
ds.get_cpu_info(42) # => "2.9 Ghz quad-core"
ds.get_cpu_price(42) # => 120
ds.get_mouse_info(42) # => "Wireless Touch"
ds.get_mouse_price(42) # => 60
# 這代表了電腦ID 42的:
# CPU 是 "2.9 Ghz quad-core", 價格是美元 $120
# 滑鼠 是 "Wireless Touch", 價格是美元 $60...(這滑鼠也太貴了吧)
這位新進員工馬上就寫了初步的解決方案如下:
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
不久,他就發現自己的程式碼存在了重複編碼的情形,於是請教了資深的同事幫忙。這位同事建議兩個方案來解決問題:(1) Dynamic Methods、 (2) Method Missing
首先注意到在 Computer 內的每個方法都有類似的程式碼,先把它抓出來再想辦法修改。
info = @data_source.get_mouse_info(@id)
price = @data_source.get_mouse_price(@id)
result = "Mouse: #{info} ($#{price})"
return "* #{result}" if price >= 100 result
還記得 send 方法嗎? 我們可以讓方法名稱變成只是字串變數"get_#{name}_info",經由#{name}變數的改變,便可以用 send() 來『 動態呼叫 』原本重複的方法。
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def component(name)
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 mouse
component :mouse
end
def cpu
component :cpu
end
def keyboard
component :keyboard
end
end
接者我們利用 define_method 可 『 動態定義 』方法的特性,把 mouse()、cpu()、及 keyboard() 再做簡化。作者建立 define_component 的類別方法,並將定義動態方法的程式碼放在裡面。然後以呼叫 define_component() 來建立實例方法,最終的輸出都是相同的。
這裡我提供另一個寫法,其實也可直接將方法名稱放在同ㄧ個集合中,再用 each do 轉出來就好。
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
#----------------- 以集合(Array)方式來做 ---------------------
titles = ['cpu', 'mouse', 'keyboard']
titles.each do |title|
define_method(title) do
info = @data_source.send("get_#{title}_info", "#{@id}")
price = @data_source.send("get_#{title}_price", "#{@id}")
result = "#{title.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
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
修改到目前為止,程式碼重複的情況其實幾乎沒有了。不過你還是可以利用 Regular Expression 讓上步驟最後三行程式碼都省略掉。 注意在 initialize() 多了ㄧ行使用正規表示法的程式碼,當 block 依附在 Enumerable#grep()方法時,這個 block 將會被與 grep 裡的 regular expression 做對比,而符合的結果會被儲存在**$1**全域變數裡。
換句話說,如果在 data_source 中有 get_cup_info() 以及 get_mouse_info() Computer.define_component(cpu) 與 Computer.define_component(mouse) 就會被呼叫和執行。
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
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})"
return "* #{result}" if price >= 100
result
end
end
end
使用 Methods Missing 就簡單許多了。如果有看過之前 Chapter 3:Methods - Part III 的文章,應該就不需要多作解釋了。
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def method_missing(name)
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
如果有任何問題,都歡迎留言詢問!