读《Ruby元编程》之方法

《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 [email protected]_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方法。个人觉得删除方法容易出问题,用白板会好一些

      使用原则

由于幽灵方法并不是真正的方法,用的时候还容易踩坑,所以在可以使用动态方法的时候,尽量使用动态方法,除非必须使用幽灵方法,否则尽量不要使用它。