使用define_method定义方法

这段代码中,Game类中的以runs_on开头的方法,可以用define_method重构, define_method

class Game
  SYSTEMS = ['SNES', 'PS1', 'Genesis']

  attr_accessor :name, :year, :system

  def runs_on_snes?
    self.system == 'SNES'
  end

  def runs_on_ps1?
    self.system == 'PS1'
  end

  def runs_on_genesis?
    self.system == 'Genesis'
  end
end

重构后的代码,不必再定义一坨坨的方法

class Game
  SYSTEMS = ['SNES', 'PS1', 'Genesis']

  attr_accessor :name, :year, :system

  SYSTEMS.each do |system|
    define_method "runs_on_#{system.downcase}?" do
      self.system = system
    end
  end
end

send 调用方法

ruby提供了send方法,可以实现对某个对象方法的调用,send方法中第一个参数是方法名,其余的是参数列表

library = Library.new(GAMES)
library.list
library.emulate("Contra")
game = library.find("Contra")

使用send方法就是这样,ruby还提供了public_send方法,public_send方法顾名思义只用于public的方法

library = Library.new(GAMES)
library.send(:list)
library.send(:emulate, "Contra")
game = library.send(:find, "Contra")

method对象

通常我们是这样调用方法的

library = Library.new(GAMES)
library.list
library.emulate("Contra")

也可以获得method对象,并使用method的call方法调用,比如上面的代码可以这样写,使用method方法获取一个Method对象

library = Library.new(GAMES)
list = library.method(:list)
list.call
emulate = library.method(:emulate)
emulate.call("Contra")

使用define_method和send重构

需重构的代码,需要把each,map,select重构,这几个方法非常的类似,可以用define_method代替

class Library
  attr_accessor :games

  def each(&block)
    games.each(&block)
  end

  def map(&block)
    games.map(&block)
  end

  def select(&block)
    games.select(&block)
  end
end

第一步,需要我们定义三个不同的方法,如下

class Library
  attr_accessor :games
  [:each, :map, :select].each do |method|
    define_method method do |&block|

    end
  end
end

然后需要用send方法调用对象的不同的方法,可以这样写

define_method method do |&block|
  game.send(method, &block)     
end

重构完就是这样的样子

class Library
  attr_accessor :games

  [:each, :map, :select].each do |method|
    define_method(method) do |&block|
      games.send(method, &block)
    end
  end
end

method_missing

当调用一个对象不存在的方法时,会触发该对象的method_missing方法,比如

我们定义一个Library类,创建对象并调用test_method("arg1","arg2")方法,触发了一个
NoMethodError的异常

irb(main):001:0> class Library
irb(main):002:1> end
=> nil
irb(main):003:0> Library.new.test_method("arg1","arg2")
NoMethodError: undefined method `test_method' for #<Library:0x2745678>
        from (irb):3
        from D:/Ruby200/bin/irb:12:in `<main>'

可以覆盖method_missing方法,来捕获NoMethodError异常,比如

class Library
  def method_missing(method_name, *args)
    puts "call method: #{method_name}, args is: #{args}"
  end
end

继续调用test_method方法,得到的结果

irb(main):013:0>  Library.new.test_method("arg1","arg2")
call method: test_method, args is: ["arg1", "arg2"]
=> nil

method_missing, define_method和send的例子

实现Library类中的method_missing方法,使用define_method和method_missing动态的定义方法,需要定义的实例方法在SYSTEM数组里,使用self.class.class_eval动态的添加实例方法,define_method定义方法,比如调用librar.pc时,触发method_missing,pc在SYSTEM数组里,然后定义pc方法,并调用find_by_system方法

class Library
  SYSTEMS = ['arcade', 'atari', 'pc']

  attr_accessor :games

  def method_missing(name, *args)
    system = name.to_s
    if SYSTEMS.include?(system)
      self.class.class_eval do
        define_method(system) do
          find_by_system(system)
        end
      end
      send(system)
    else
      super
    end
  end

  private

  def find_by_system(system)
    games.select { |game| game.system == system }
  end
end

respond_to?

在ruby中,可以使用respond_to?判断某个对象是否可以响应某个方法

比如"hello" 能够响应upcase方法

irb(main):017:0> "hello".respond_to?(:upcase)
=> true

当然,我们也可以自己定义respond_to?方法,比如Library类例子中,我们动态的定义了'arcade', 'atari', 'pc'方法,但是Library类的实例是不会响应动态定义的方法的。我们需要自己定义Library类的respond_to?方法

代码

def respond_to?(name)
  SYSTEMS.include?(name.to_s) || super
end

当方法名在SYSTEMS数组是,返回true,代表响应相应的方法

other posts
- http://www.alfajango.com/blog/method_missing-a-rubyists-beautiful-mistress/
- http://ruby-china.org/topics/3434
- http://ruby-china.org/topics/4313


lidashuang
6.7k 声望165 粉丝

$ Ruby/Elixir/Golang


引用和评论

0 条评论