Ruby元编程-Week-2

来源:互联网 发布:blog与mysql的分离 编辑:程序博客网 时间:2024/04/30 14:35

更多文章欢迎来沈小黑的菜园转转啦啦啦~~

解决代码重复

在星期二,书中给出了一个关于包装老系统接口造成代码冗余的例子。下面是这个例子,它贯穿了整个章节,集中体现了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    endend

看起来简单了不少,至少所有的调用逻辑都放在同一个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 :keyboardend

这样就不用一个个定义方法了,只需要在类中增加一条代码就会生成一个对应的方法,看起来又好了很多。

但是还是有问题:假如哪天采购部心血来潮新加了个数位板什么的,我们还要修改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  endend

很完美,那么还有其他的方法么?

幽灵方法

在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  endend

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    endend

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            #...    endend

幽灵方法的陷阱

无限循环的bug

如果我们写了如下一个程序:

class Roulette    def method_missing(name, *args)        3.times do            number = rand(10)+1        end        "#{name} got a #{number}"    endend

这个程序会执行出无限循环最终崩溃的结果,为什么会这样呢?

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  # ... codesend

小结

大多数情况下,幽灵方法都不如动态方法来得好,因为它并不是真正的方法,而只是类似异常的一个功能,使用它会导致诸如难以调试、方法被定义等问题。在能使用动态方法完成需求的场景下,我们都应该优先考虑动态方法而不是幽灵方法。当然,也有很多情况下不得不使用幽灵方法,那时候就是幽灵方法大显神威挥起它的镰刀的时候啦~~

0 0
原创粉丝点击