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

  • Recipe 2.13, "Simulating a Subclass of Fixnum"
  • Recipe 8.8, "Delegating Method Calls to Another Object," for an alternate implementation of delegation that's usually easier to use


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net