Overriding method_missing()

你几乎不大可能亲自调用 method_missing() 方法。不过,你可以覆写它来截获无主的消息。每一个来到 method_missing() 办公桌上的消息都带着调用方法的名字,以及所有调用时传递的参数和块。

  class Lawyer
    def method_missing(method, *args)
      puts "You called: #{method}(#{args.join(', ')})"
      puts "(You also passed it a block)" if block_given?
    end
  end

  bob = Lawyer.new
  bob.talk_simple('a', 'b') do
    # a block
  end
  #=>  You called: talk_simple(a, b)
  #=>  (You also passed it a block)

Ghost Methods

当需要定义很多相似的方法时,可以通过响应 method_missing 方法来免去一些手工定义这些方法的工作。这点像是在告诉这个对象,“如果别人问你一些不理解的东西,就这样做”。 被 method_missing 方法处理的消息,从调用者的角度看,跟普通方法没什么区别,但是实际上接受者并没有相对应的方法,所以这被称为一个 幽灵方法

来自 Ruport 的例子

Ruport 是一个 Ruby 报表库。你可以通过调用 Ruport::Data::Table 类来创建表格数据及把它们转换为不同的格式–比如文本格式:

  require 'ruport'

  table = Ruport::Data::Table.new :column_names => ["country", "wine"],
                                  :data => [["France", "Bordeaux"],
                                           ["Italy", "Chianti"],
                                           ["France", "Chablis"]]
  puts table.to_texts
  #=> +--------------------+
  #=> | country |   wine   |
  #=> +--------------------+
  #=> | France  | Bordeaux |
  #=> | Italy   | Chianti  |
  #=> | France  | Chablis  |
  #=> +--------------------+

又比如你想仅仅选择法国红酒的数据,并把它们转换为逗号分隔的数据:

  found = table.rows_with_country("France")
  found.each do |row|
    puts row.to_csv
  end
  #=> France, Bordeaux
  #=> France, Chablis

你所做的只是调用了 Ruport::Data::Table 类中一个名为 rows_with_country 的方法,不过这个类的作者怎么能预见到你的数据中会有一个名为 country 的列呢?事实上,该作者对此一无所知。如果深入研究 Ruport 的代码,则会看到 rows_with_country 和 to_csv 都是幽灵方法:

  class Table
    def method_missing(id, *arg, &block)
      return as($1.to_sym, *arg, &block) if id.to_s =~ /^to_(.*)/
      return rows_with($1.to_sym => arg[0]) if id.to_s =~ /^rows_with_(.*)/
      super
    end
    # ...

在这段代码中,对 rows_with_country() 的调用被转换为调用一个看起来比较传统的方法—-rows_with(:country),它接受列名作为参数。同样,对 to_csv() 的调用被转换为对 as(:csv) 的调用。如果调用的方法名不是以这两种模式打头,那么 Ruport 会转而调用 Kernel#method_missing 方法,它会抛出一个 NoMethodError 错误,这就是 super 关键字所做的事情。

来自 OpenStruct 的例子

OpenStruct 类来自 Ruby 标准库,一个 OpenStruct 对象的属性用起来就像是 Ruby 的变量,如果想要一个新的属性,那么只需要给它赋个值就行了,然后它就奇迹般地存在了:

  require 'ostruct'

  icecream = OpenStruct.new
  icecream.flavor = "strawberry"
  icecream.flavor                  #=> "strawberry"

这种工作方式背后的原因在于,一个 OpenStruct 对象的属性实际上是幽灵方法。 OpenStruct#method_missing 方法会捕捉对 flavor=() 方法的调用,再砍掉最后那个“=”以获得属性的名字,然后它把属性名和对应的值存放在一个哈希表中。当调用一个不以“=”结尾的方法时,method_missing 方法会在哈希表中查找相应的方法名,并返回对应的值。 OpenStruct 中的代码看起来有点复杂,因为它要处理一些诸如错误处理这样的特殊情况。不过,你可以很容易写一个简化版的开放结构类:

  class MyOpenStruct
    def initialize
      @attributes = {}
    end

    def method_missing(name, *args)
      attribute = name.to_s
      if attribute =~ /=$/
        @attributes[attribute.chop] = args[0]
      else
        @attributes[attribute]
      end
    end
  end

  icecream = MyOpenStruct.new
  icecream.flavor = "vanilla"
  icecream.flavor              #=> "vanilla"