Protected Methods and Ruby 2.0
Sep 7, 2012 @ 10:24 amTL;DR: respond_to?
will return false
for protected methods in Ruby 2.0
Let’s check out how protected
and private
methods behave in Ruby. After
that, we’ll look at how Ruby 2.0 changes could possibly break your code (and
what to do about it).
Method Visibility
In Ruby, we have three visibilities: public, protected, and private. Let’s define a class with all three:
class Heart
def public_method; end
protected
def protected_method; end
private
def private_method; end
end
First, let’s see how these differ from within the Heart
class.
Internal Visibility
Inside the Heart
class, we can call any of these methods with an implicit
recipient. In other words, this method will not raise exceptions (note that I’m
just reopening the Heart
class for demonstration):
class Heart
def ok!
public_method
protected_method
private_method
end
end
Public and protected methods can be called with an explicit recipient, but private methods cannot. So the following code will raise an exception on the third line of the method body:
class Heart
def not_ok!
self.public_method # OK
self.protected_method # OK
self.private_method # raises NoMethodError
end
end
External Visibility
Outside the Heart
class, we can only call the public methods:
irb(main):032:0> heart = Heart.new
=> #<Heart:0x007fdad1952f78>
irb(main):033:0> heart.public_method # => nil
irb(main):034:0> heart.protected_method # => raises NoMethodError
irb(main):035:0> heart.private_method # => raises NoMethodError
One notable exception is if the object sending the message is of the same type as the object receiving the message, then it’s OK to call protected methods.
Here is an example:
class Hands < Heart
def call_stuff r
r.public_method # => ok!
r.protected_method # => ok, but only if self.is_a?(r.class)
r.private_method # => raises NoMethodError
end
end
I find this behavior to be most useful when implementing equality operators. For example:
class A
def == other
if self.class == other.class
internal == other.internal
else
super
end
end
protected
def internal; :a; end
end
Introspection
Finally, let’s look at respond_to?
. The behavior of this method is changing
in Ruby 2.0.0. First we’ll look at the behavior in 1.9, then how it changes in
Ruby 2.0.0.
The respond_to?
method will return true if the object responds to the given
method. Let’s call respond_to?
on our Heart
object (with Ruby 1.9) and see
what it returns:
1.9.3-p194 :010 > heart = Heart.new
=> #<Heart:0x007faaaa14e450>
1.9.3-p194 :011 > heart.respond_to? :public_method # => true
1.9.3-p194 :012 > heart.respond_to? :protected_method # => true
1.9.3-p194 :013 > heart.respond_to? :private_method # => false
Ruby 1.9 will return true for public and protected methods, but false for
private methods. If we compare this to actually calling the method, we’ll see
an inconsistent behavior. Let’s interleave respond_to?
checks along with
calling the method to see what happens:
1.9.3-p194 :014 > heart = Heart.new
=> #<Heart:0x007faaaa16d080>
1.9.3-p194 :015 > heart.respond_to? :public_method # => true
1.9.3-p194 :016 > heart.public_method # => nil
1.9.3-p194 :017 > heart.respond_to? :protected_method # => true
1.9.3-p194 :018 > heart.protected_method # => NoMethodError
1.9.3-p194 :019 > heart.respond_to? :private_method # => false
1.9.3-p194 :020 > heart.private_method # => NoMethodError
So, despite the fact that respond_to?
returns true for the protected method,
we cannot actually call that method.
Introspection (in Ruby 2.0.0)
In Ruby 2.0.0, respond_to?
has changed. It no longer returns true for
protected methods. Let’s look at our Heart
example again, but this time with
Ruby 2.0.0:
irb(main):013:0> heart = Heart.new
=> #<Heart:0x007fce0b09a188>
irb(main):014:0> heart.respond_to? :public_method # => true
irb(main):015:0> heart.public_method # => nil
irb(main):016:0> heart.respond_to? :protected_method # => false
irb(main):017:0> heart.protected_method # => NoMethodError
irb(main):018:0> heart.respond_to? :private_method # => false
irb(main):019:0> heart.private_method # => NoMethodError
The behavior of respond_to?
lines up with the reality of calling the method in
Ruby 2.0.0.
Caveats on Reality
The changes to respond_to?
also apply inside our “same instances” case. Let’s
use this class as an example:
class A
def == a
puts a.respond_to? :zoom!
puts a.zoom!
end
protected
def zoom!; :a; end
end
If we run the following code in Ruby 2.0.0, the call to respond_to?
will
return false despite the fact that we can actually call the method:
irb(main):029:0> A.new == A.new
false
a
=> nil
I’m not sure this is a big problem because we should be checking ancestors in
the comparator methods. If we check that the ancestors are the same, then the
respond_to?
calls become unnecessary. Also 99% of the objects I write
don’t implement object comparator methods.
Compatibility
Most of the problems I’ve found in the Rails code base relating to respond_to?
were fixed by either changing the visibility of the method, or calling
respond_to?
with a true
as the second argument. In 1.9, the true
tells
Ruby to search private methods, and in 2.0, private and protected methods.
For library authors, dealing with this change depends on the situation. For example, if you have code like this:
def some_method other
if other.respond_to?(:foo)
other.foo
else
some_default_behavior
end
end
Consider forcing the other
object to have the method foo
, and the super
class of the foo
instance implementing some_default_behavior
.
If you expect foo
to be a protected method, consider changing to is_a?
checks, or passing true
to respond_to?
. Passing true could result in false
positives, but I haven’t personally encountered that as a problem (yet).
Happy Hacking! <3<3<3<3