Ruby元编程 星期二
@ Shen Jianan · Saturday, Oct 24, 2015 · 6 minute read · Update at Oct 24, 2015

解决代码重复

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

小结

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

About Me

2018.02至今 杭州嘉云数据 算法引擎

2017.6-2017.12 菜⻦网络-⼈工智能部-算法引擎

2016.09-2018.06 南京大学研究生

2015.07-2015.09 阿里巴巴-ICBU-实习

2012.09-2016.06 南京大学本科