Don't forget about respond_to? when implementing method_missing
method_missing
can be used to do some really cool things in Ruby, but it can also cause some headaches when done improperly. One cause for headache is when method_missing
comes with a broken or missing respond_to?
implementation.
Example Proxy class
Consider the following example of a proxy class that uses method_missing
, but doesn’t implement respond_to?
.
class Proxy
def initialize(subject)
@subject = subject
end
def method_missing(method)
@subject.send(method)
end
end
proxy = Proxy.new(Time)
proxy.respond_to?(:now) # => false
proxy.now # => Fri Feb 05 00:34:53 -0500 2010
Our proxy object is a dirty liar. When we ask if it responds to now
it returns false
, but when we actually call now
it responds successfully.
Here’s a better implementation for Proxy:
class Proxy
def initialize(subject)
@subject = subject
end
def method_missing(method)
if @subject.respond_to?(method)
@subject.send(method)
else
super(method)
end
end
def respond_to?(method, include_private = false)
super || @subject.respond_to?(method, include_private)
end
end
proxy = Proxy.new(Time)
proxy.respond_to?(:now) # => true
proxy.now # => Fri Feb 05 00:34:53 -0500 2010
That’s better. We added an implementation for respond_to?
and some calls to super
. Keep in mind that we’re overriding existing methods, and we want to add behavior to them unobtrusively. You can learn some techniques for DRYing and testing method_missing
and respond_to?
at Technical Pickles.
Let’s examine a bug with respond_to?
in a Rails plugin you’re probably using…
will_paginate and respond_to?
Here is a Post model that contains a class method called paginate_by_something
.
# will_paginate is installed
class Post < ActiveRecord::Base
def self.paginate_by_something
# "something" is not an attribute
end
end
Post.respond_to?(:paginate_by_something) # => false
What!? We explicitly define a class level method, but when we interrogate it with respond_to?
it returns false
.
The problem is with will_paginate’s respond_to?
. Below is a snippet of code from will_paginate that gets mixed into your models.
def respond_to?(method, include_priv = false) #:nodoc:
case method.to_sym
when :paginate, :paginate_by_sql
true
else
super(method.to_s.sub(/^paginate/, 'find'), include_priv)
end
end
The first thing will_paginate does is return true for its obvious methods: paginate
and paginate_by_sql
. Then the interesting part of this code is the call to super
. It turns a method like paginate_by_author
into find_by_author
, which respond_to?
would return true
for, if it is an ActiveRecord dynamic finder. Going back to the paginate_by_something
example in our Post model, we can see that respond_to?
returns false
because find_by_something
is NOT a dynamic finder (when “something” is not an attribute).
The fix to will_paginate is fairly simple, and I’ve already submitted a patch. Props to Ryan Briones for pairing with me while debugging the problem on a project at Chase.
On a side note, if you have a particular RSpec test that fails when you run the whole test suite, but pass when ran individually; Check your mocks on methods that don’t implement respond_to?
correctly. RSpec uses respond_to?
internally when using mocks, and RSpec expects it to work!