在《 Ruby 元编程》一书的第二章 method
中,通过一段代码的重构,来展示 Ruby 的特性,如何以很少的代码来达到我们最终的效果。
示例 Demo
源代码:data_source.rb,其通过传入工作站点的 id,用来获取工作站点信息,如下:
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
调用代码如下:
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
重构
可以看到在 DS
类中,有很多重复的信息。第一步,首先将其抽象成一个 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
但是,抽象成这个类中,可以看到方法中对 data_source
的使用还有点信息的冗余。这里可以对方法再以参数的形式调用,如下。
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
方法的参数指定一个方法的名称和参数,这样对方法的调用就可以抽象在 component
方法中。
调用代码:
my_computer = Computer.new(42, DS.new)
my_computer.cpu # => * Cpu: 2.16 Ghz ($220)
PS: 这种动态派发的这种特殊用法有时被称为**模式派发 (Pattern Dispatch),因为它基于方法名的某种模式来过滤方法。
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
方法,来在运行时动态地定义方法,也称** 动态方法 (Dynamic Method)**。
3. 内省代码的使用
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
这里通过使用正则表达式,来进一步简化方法的定义。使用 grep
,当满足之后的正则表达式,则会定义相应的方法。
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 方法(幽灵方法),是指在方法的调用过程中,若是在其类型中及其祖先链上找不到相应的方法,则会在实例上调用 method_missing
方法(其属于 Kernel 的一个实例方法,而所有的对象都继承自 kernel 模块)。这里通过重写 method_missing
方法,来达到对 data_source
中相应的方法的动态调用。