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
Example Proxy class
Consider the following example of a proxy class that uses
method_missing, but doesn’t implement
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
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
# 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
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_by_sql. Then the interesting part of this code is the call to
super. It turns a method like
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
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!