《Ruby元编程》之方法笔记。
化繁为简
《Ruby元编程》的第3章说到了方法,从一段代码的重构说起,主线是动态方法dynamic methods和幽灵方法ghost methods,随后说了下ghost methods 的两个常见陷阱。
整章看下来,最大的感触就是重构后的代码真的是清爽啊。dynamic methods和ghost methods简直就是化繁为简的利器。
先来看段代码。
一段需要重构的代码
引用书中的例子:
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = 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
这段代码中,class Computer有三个method,且每个method的代码有很多共通的部分。如何解决这种代码繁复的问题?
解决方法就在dynamic methods和ghost methods。
动态方法dynamic methods
先来看看用动态方法如何重构上诉代码:
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = 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得到@data_source中所有匹配/^get_(.*)_info$/
的methods,使用$1来保存,然后调用类方法define_component。而在类方法define_component中,则使用了动态方法,其中,define_method是定义动态方法,而send则是动态派发。
方法无非就是定义加调用,动态方法跟普通方法并没有什么大的区别。
定义动态方法
用define_method来定义动态方法。看个例子:
class MyClass define_method :my_method do |x, y| x + x * y end end obj = MyClass.new obj.send(:my_method,1,2) # => 3 obj.my_method(1,2) # => 3
方法的定义由常规的def关键字变成了define_method关键字, 其他并没有什么变化。方法名作为参数,外加一个代码块block。我的理解是,它的灵活性与send其实很一致,就在于方法名是参数,可以动态定义方法。
调用动态方法
调用一个方法实际上是给对象发送一条信息。
send是Object的一个实例方法,第一个参数是方法的名字,由于方法名最好是不可修改的,所以通常是以符号的形式传递,剩下的,则是需要传递给方法的参数,可含代码块。
看个例子:
class MyClass def my_method(x,y) x + yield(x,y) end end obj = MyClass.new obj.send(:my_method,1,2){|x, y| x * y} # => 3 obj.my_method(1,2) {|x, y| x * y} # => 3
使用send调用my_method, 和使用.my_method的调用是一样的。
那么使用send的意义在哪里?因为方法名成了参数,所以在代码运行中,可以决定调用哪个方法,上面重构后的代码便是如此,根据name的值,来决定需要调用哪个method。
幽灵方法 ghost methods
见到了大名鼎鼎的method_missing了。
先看用幽灵方法重构Computer类的结果:
class Computer < BasicObject
def initialize(computer_id, data_source)
@id = computer_id
@data_source = 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", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.capitalize}: #{info}($#{price})"
return "#{result}" if price >= 100
result
end
end
这里,注意到Computer继承自BasicObject,而不是默认的Object,这是为了避开method_missing的一个陷阱,而选择了BasicObject这个白板类(blank slates)【白板类:拥有极少方法的类】,这块留在陷阱部分说。
可以看到method_missing中send的身影,故它也使用了动态派发。
method_missing是BasicObject的一个私有实例方法。通过重定义method_missing来完成对方法调用的拦截,但是值得一提的是,它们不会出现在object#methods的列表中,故有幽灵方法之称。
书中举了Ghee的例子,我去看了它的method_missing定义, 在lib/ghee的resource_proxy.rb中:
## lib/ghee/resource_proxy.rb
def method_missing(message, *args, &block)
subject.send(message, *args, &block)
end
这里连续调用了两次method_missing,先是将message转发给Ghee::ResourceProxy#method_missing方法,然后再从这里转给subject,而这个subject是一个Hassher::Mash对象,它又转给了Hassher::Mash#method_missing方法进行处理。这种捕获幽灵方法,并将它们转发给另一个对象,又称之为幽灵代理,即像Ghee::ResourceProxy这样的对象,就是幽灵代理。
幽灵方法的两个陷阱
在method_missing中调用了未定义的method,导致method_missing被不断回调,直到调用堆栈溢出
看段代码:
class C def method_missing(name, *args) 10.times do number = rand(10) end puts "#{number}" end end
【这是一段没啥用的代码,仅仅用来作为例子】
这里其实涉及到作用域scope的问题,number定义在代码块中,等到代码块结束,执行puts “#{number}”时,ruby会把number当成是一个在self上省略了括号的方法调用,即self.number, 正常情况下,会出现nomethoderror,因为没有定义number这个method的,但是由于重写了method_missing,所以它会继续调用这个重写后的method_missing,导致出现死循环。
如何解?改number的作用域即可。
class C def method_missing(name, *args) number = 0 10.times do number = rand(10) end puts "#{number}" end end
幽灵方法与真实方法重名,导致幽灵方法被忽略。
书中举了一个display的例子,我觉得挺好理解的,附上:
my_computer = Computer.new(12, DS.new) my_computer.display # => nil
这里,my_computer调用了display方法,其本意是要看显示器method,但是Object自带了一个display方法,Computer继承Object,所以也有一个display方法,导致调用的时候,忽略了幽灵方法,这里也可以看得出来,在调用方法时,必然是先搜遍了祖先链,然后再调用幽灵方法。
怎么破?两种方式。
使用白板类
用幽灵方法重构代码那部分就提到了,Computer继承自BasicObject,用的就是白板类,从而没有了display这样的情况。
删除方法display
用两种删除方法的途径:undef_method【删除所有包括继承来的方法】, remove_method【删除接受者自己的方法】
看段代码:
class Computer ..... def self.hide(name) if instance_methods.include?(name) undef_method name end end hide('display') end
这样就会删除掉继承自Object#display方法。个人觉得删除方法容易出问题,用白板会好一些
使用原则
由于幽灵方法并不是真正的方法,用的时候还容易踩坑,所以在可以使用动态方法的时候,尽量使用动态方法,除非必须使用幽灵方法,否则尽量不要使用它。