Responding to Calls to Undefined Methods

Problem

Rather than having Ruby raise a NoMethodError when someone calls an undefined method on an instance of your class, you want to intercept the method call and do something else with it.

Or you are faced with having to explicitly define a large (possibly infinite) number of methods for a class. You would rather define a single method that can respond to an infinite number of method names.

Solution

Define a method_missing method for your class. Whenever anyone calls a method that would otherwise result in a NoMethodError, the method_missing method is called instead. It is passed the symbol of the nonexistent method, and any arguments that were passed in.

Here's a class that modifies the default error handling for a missing method:

class MyClass def defined_method 'This method is defined.' end def method_missing(m, *args) "Sorry, I don't know about any #{m} method." end end o = MyClass.new o.defined_method # => "This method is defined." o.undefined_method # => "Sorry, I don't know about any undefined_method method."

In the second example, I'll define an infinitude of new methods on Fixnum by giving it a method_missing implementation. Once I'm done, Fixnum will answer to any method that looks like "plus_#" and takes no arguments.

class Fixnum def method_missing(m, *args) if args.size > 0 raise ArgumentError.new("wrong number of arguments (#{args.size} for 0)") end match = /^plus_([0-9]+)$/.match(m.to_s) if match self + match.captures[0].to_i else raise NoMethodError. new(" undefined method '#{m}' for #{inspect}:#{self.class}") end end end 4.plus_5 # => 9 10.plus_0 # => 10 -1.plus_2 # => 1 100.plus_10000 # => 10100 20.send(:plus_25) # => 45 100.minus_3 # NoMethodError: undefined method 'minus_3' for 100:Fixnum 100.plus_5(105) # ArgumentError: wrong number of arguments (1 for 0)

 

Discussion

The method_missing technique is frequently found in delegation scenarios, when one object needs to implement all of the methods of another object. Rather than defining each method, a class implements method_missing as a catch-all, and uses send to delegate the "missing" method calls to other objects. The built-in delegate library makes this easy (see Recipe 8.8), but for the sake of illustration, here's a class that delegates almost all its methods to a string. Note that this class doesn't itself subclass String.

class BackwardsString def initialize(s) @s = s end def method_missing(m, *args, &block) result = @s.send(m, *args, &block) result.respond_to?(:to_str) ? BackwardsString.new(result) : result end def to_s @s.reverse end def inspect to_s end end

The interesting thing here is the call to Object#send. This method takes the name of another method, and calls that method with the given arguments. We can delegate any missing method call to the underlying string without even looking at the method name.

s = BackwardsString.new("I'm backwards.") # => .sdrawkcab m'I s.size # => 14 s.upcase # => .SDRAWKCAB M'I s.reverse # => I'm backwards. s.no_such_method # NoMethodError: undefined method 'no_such_method' for "I'm backwards.":String

The method_missing technique is also useful for adding syntactic sugar to a class. If one method of your class is frequently called with a string argument, you can make object.string a shortcut for object.method("string"). Consider the Library class below, and its simple query interface:

class Library < Array def add_book(author, title) self << [author, title] end def search_by_author(key) reject { |b| !match(b, 0, key) } end def search_by_author_or_title(key) reject { |b| !match(b, 0, key) && !match(b, 1, key) } end :private def match(b, index, key) b[index].index(key) != nil end end l = Library.new l.add_book("James Joyce", "Ulysses") l.add_book("James Joyce", "Finnegans Wake") l.add_book("John le Carre", "The Little Drummer Boy") l.add_book("John Rawls", "A Theory of Justice") l.search_by_author("John") # => [["John le Carre", "The Little Drummer Boy"], # ["John Rawls", "A Theory of Justice"]] l.search_by_author_or_title("oy") # => [["James Joyce", "Ulysses"], ["James Joyce", "Finnegans Wake"], # ["John le Carre", "The Little Drummer Boy"]]

We can make certain queries a little easier to write by adding some syntactic sugar. It's as simple as defining a wrapper method; its power comes from the fact that Ruby directs all unrecognized method calls to this wrapper method.

class Library def method_missing(m, *args) search_by_author_or_title(m.to_s) end end l.oy # => [["James Joyce", "Ulysses"], ["James Joyce", "Finnegans Wake"], # ["John le Carre", "The Little Drummer Boy"]] l.Fin # => [["James Joyce", "Finnegans Wake"]] l.Jo # => [["James Joyce", "Ulysses"], ["James Joyce", "Finnegans Wake"], # ["John le Carre", "The Little Drummer Boy"], # ["John Rawls", "A Theory of Justice"]]

You can also define a method_missing method on a class. This is useful for adding syntactic sugar to factory classes. Here's a simple factory class that makes it easy to create strings (as though this weren't already easy):

class StringFactory def StringFactory.method_missing(m, *args) return String.new(m.to_s, *args) end end StringFactory.a_string # => "a_string" StringFactory.another_string # => "another_string"

As before, an attempt to call an explicitly defined method will not trigger method_missing:

StringFactory.superclass # => Object

The method_missing method intercepts all calls to undefined methods, including the mistyped names of calls to "real" methods. This is a common source of bugs. If you run into trouble using your class, the first thing you should do is add debug statements to method_missing, or comment it out altogether.

If you're using method_missing to implicitly define methods, you should also be aware that Object.respond_to? returns false when called with the names of those methods. After all, they're not defined!

25.respond_to? :plus_20 # => false

You can override respond_to? to fool outside objects into thinking you've got explicit definitions for methods you've actually defined implicitly in method_missing. Be very careful, though; this is another common source of bugs.

class Fixnum def respond_to?(m) super or (m.to_s =~ /^plus_([0-9]+)$/) != nil end end 25.respond_to? :plus_20 # => true 25.respond_to? :succ # => true 25.respond_to? :minus_20 # => false

 

See Also

Категории