本篇要分享的是此书(在第三章)我蛮喜欢的范例之一,作者以说故事的方式讲解本章节所介绍的题目,假设的情境是新进员工被会计部门赋予一项任务,目标是找出花费大於美金$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
如果有任何问题,都欢迎留言询问!
GCP IAP 今天再来了解一下什麽事IAP?他的全名即是dentity-Aware Proxy简称...
2021/01/15再用https://developer.android.com/studio 的...
资料型态 内建资料型态是构成整个程序的最小型态单位,是程序中不可或缺的元素,而Dart的内建类型主要...
完赛检讨 资料处理 虽然我们有大致上把红框等杂讯去除掉,但我们还是没有完全把照杂讯清除乾净。 Yol...
之前我们有提到,JavaScript 如何与浏览器沟通,於是我们讨论到透过 JavaScript 取...