解决代码重复
在星期二,书中给出了一个关于包装老系统接口造成代码冗余的例子。下面是这个例子,它贯穿了整个章节,集中体现了Ruby道路的优越性+_+
有一个老系统,他有很多蹩脚的代码,现在要求系统自动为超过99美元的开销添加标记。
蹩脚的代码是这样的:
class DS
def initialize #连接数据源
def get_cpu_info(workstation_id)
def get_cpu_price(workstation_id)
def get_mouse_info(workstatin_id)
def get_mouse_price(workstation_id)
def get_keyboard_info(workstation_id)
def get_keyboard_price(workstatin_id)
def get_display_info(workstation_id)
def get_display_price(workstatin_id)
真够蹩脚的= =如果用简单的包装方法来完成这个需求的话,代码就会变成这样:
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
#cpu, keyboard等都是相似的代码
#....
end
原谅我照抄书上的代码吧,要我写我也这么写+_+
呐~我们发现这些不同的部件代码都是相似的~怎么解决这个问题呢?首先祭出Ruby中的动态方法。
动态方法
动态方法分成两个部分:动态调用方法和动态创建方法。
动态调用方法
我们可以发现,这里的调用方法名其实是相似的,改变的只是 “get_#{设备名}_price” 中的facility。那么有没有办法可以直接把需要的设备名包装成 “get_#{设备名}_price” 的方法呢?
其实调用方法实际上是给一个对象发送消息,而Ruby正提供了一个send方法来支持通过发送消息的方式调用方法。
把这个技巧运用到Computer类中:
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.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
看起来简单了不少,至少所有的调用逻辑都放在同一个component方法里面了。但是仍然感觉有点不足:mouse、cpu、keyboard方法几乎都没有做什么事情,只是调用了component方法,这显得很多余。怎么解决这个问题呢?现在轮到动态创建方法登场了。
其实用send需要注意的一点是它可以调用private方法,如果确定只需要调用public方法的话,最好还是public_send方法吧。
动态创建方法
如果说动态调用方法在Java的语言机制中还能通过一定步骤实现的话,动态创建方法应该是Ruby的“独门绝技”了~Ruby可以通过Module#define_method方法随时定义一个方法,只需要提供方法名和充当方法主体的块。
再把动态创建方法糅合进Computer类中:
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
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
这样就不用一个个定义方法了,只需要在类中增加一条代码就会生成一个对应的方法,看起来又好了很多。
但是还是有问题:假如哪天采购部心血来潮新加了个数位板什么的,我们还要修改Computer类,这样耦合性是不是就有点高了?也许我们可以把DS类中所有"get_#{设备名}_price"格式的方法都读取出来,然后对应地创建动态方法,这就用到了Ruby的内省特性。
利用内省优化
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
data_source.public_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
很完美,那么还有其他的方法么?
幽灵方法
在Ruby中,假如调用了一个找不到的方法,就会触发一个类似异常的,叫做method_missing的方法。所有找不到的方法都会跑到这里,那我们就可以通过修改method_missing来实现动态代理,做到神不知鬼不觉地在某些情况下完成某些功能。比如,我们根本没有定义cpu方法,但是在method_missing中增加了“如果找不到的方法名是cpu,那么返回cpu信息”的逻辑,那么依然可以实现我们的需求,而不需要定义任何新方法。
利用幽灵方法重构Computer
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") #如果是其他的未定义方法,那就给默认的method_missing处理
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
respond_to_missing?方法
如果调用Computer.respond_to?(:mouse) 它会返回false,因为根本就没有定义这个方法嘛~那假如想要让幽灵方法中处理的情况也能体现在respond_to?方法中呢?我们不必修改respond_to?方法,而可以修改respond_to_missing?方法。因为respond_to?会检查respond_to_missing?,如果返回为true的话,就可以判定为true了。
所以在这里,我们需要把respond_to_missing?改写:
class Computer
#...
def respond_to_missing?(method, include_private = false)
@data_source.respond_to?("get_#{method}_info") || super
end
end
P.S. 还有一个const_missing?方法,作用跟method_missing?类似,只是处理的是常量找不到的问题。如Rake中为兼容而允许在后续版本中使用先前没有命名空间的老名字,就是这么实现的:
class Module
def const_missing?(const_name)
case const_name
when :Task
Rake.application.const_warning(const_name)
Rake::Task
when :FileTask
Rake.application.const_warning(const_name)
Rake:: FileTask
when :FileCreationTask
#...
end
end
幽灵方法的陷阱
无限循环的bug
如果我们写了如下一个程序:
class Roulette
def method_missing(name, *args)
3.times do
number = rand(10)+1
end
"#{name} got a #{number}"
end
end
这个程序会执行出无限循环最终崩溃的结果,为什么会这样呢?
number在循环体外使用了,这时候Ruby会认为这是一个方法,然而又找不到这个方法,这时候就会再次触发method_missing方法,如此循环下去,就会不断触发,最终导致崩溃。
这就是幽灵方法的第一个陷阱:在method_missing中的未定义方法会导致无限循环直至崩溃。
白板类
幽灵方法的另一个陷阱在于:它只有在方法找不到的时候才会被调用,所以假如祖先类或者自己后来增加了这个方法,那么就不会触发method_missing,也就会导致幽灵方法失效。
为了解决这个问题,Ruby提供了白板类BasicObject,它只有很少的几个实例方法:
p BasicObject.instance_methods
#=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]
如果需要白板类,可以直接从BasicObject继承。
在某些情况下可能需要自己决定删除什么方法,这时候可以使用Module#undef_method或者Module#remove_method,前者会删除包括继承而来的所有方法,而后者只会删除接收者自己的方法,而保留继承的方法。
现在,我们也许应该让Computer继承BasicObject了:
class Computer<BasicObject
# ... codes
end
小结
大多数情况下,幽灵方法都不如动态方法来得好,因为它并不是真正的方法,而只是类似异常的一个功能,使用它会导致诸如难以调试、方法被定义等问题。在能使用动态方法完成需求的场景下,我们都应该优先考虑动态方法而不是幽灵方法。当然,也有很多情况下不得不使用幽灵方法,那时候就是幽灵方法大显神威挥起它的镰刀的时候啦~~