在《 Ruby 元編程》一書的第二章 method 中,通過一段代碼的重構(gòu),來展示 Ruby 的特性,如何以很少的代碼來達(dá)到我們最終的效果。
示例 Demo
源代碼:data_source.rb,其通過傳入工作站點(diǎn)的 id,用來獲取工作站點(diǎn)信息,如下:
class DS
def initialize # connect to data source...
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_cpu_info(workstation_id) # ...
def get_cpu_price(workstation_id) # ...
def get_display_info(workstation_id) # ...
def get_display_price(workstation_id) # ...
# ...and so on
end
調(diào)用代碼如下:
ds = DS.new
ds.get_cpu_info(42) # => 2.16 Ghz
ds.get_cpu_price(42) # => 150
ds.get_mouse_info(42) # => Dual Optical
ds.get_mouse_price(42) # => 40
重構(gòu)
可以看到在 DS 類中,有很多重復(fù)的信息。第一步,首先將其抽象成一個(gè) Computer 的類:
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
但是,抽象成這個(gè)類中,可以看到方法中對(duì) data_source 的使用還有點(diǎn)信息的冗余。這里可以對(duì)方法再以參數(shù)的形式調(diào)用,如下。
1.使用 Object 類的 send 方法:
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
component :mouse
end
def cpu
component :cpu
end
def keyboard
component :keyboard
end
def component(name)
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.to_s.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
send 方法的參數(shù)指定一個(gè)方法的名稱和參數(shù),這樣對(duì)方法的調(diào)用就可以抽象在 component 方法中。
調(diào)用代碼:
my_computer = Computer.new(42, DS.new)
my_computer.cpu # => * Cpu: 2.16 Ghz ($220)
PS: 這種動(dòng)態(tài)派發(fā)的這種特殊用法有時(shí)被稱為**模式派發(fā) (Pattern Dispatch),因?yàn)樗诜椒哪撤N模式來過濾方法。
2.方法 define_method
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def self.define_component(name)
define_method(name) {
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.to_s.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
}
end
define_component :mouse
define_component :cpu
define_component :keyboard
end
使用 define_method 方法,來在運(yùn)行時(shí)動(dòng)態(tài)地定義方法,也稱** 動(dòng)態(tài)方法 (Dynamic Method)**。
3. 內(nèi)省代碼的使用
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) {
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
這里通過使用正則表達(dá)式,來進(jìn)一步簡化方法的定義。使用 grep,當(dāng)滿足之后的正則表達(dá)式,則會(huì)定義相應(yīng)的方法。
4.method_missing 的使用
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def method_missing(name, *args)
super if !@data_source.respond_to?("get_#{name}_info")
info = @data_source.send("get_#{name}_info", args[0])
price = @data_source.send("get_#{name}_price", args[0])
result = "#{name.to_s.capitalize}: #{info} ($#{price})"
return " * #{result}" if price >= 100
result
end
end
method_missing 方法又稱 ghost 方法(幽靈方法),是指在方法的調(diào)用過程中,若是在其類型中及其祖先鏈上找不到相應(yīng)的方法,則會(huì)在實(shí)例上調(diào)用 method_missing 方法(其屬于 Kernel 的一個(gè)實(shí)例方法,而所有的對(duì)象都繼承自 kernel 模塊)。這里通過重寫 method_missing 方法,來達(dá)到對(duì) data_source 中相應(yīng)的方法的動(dòng)態(tài)調(diào)用。