Tenderlove Making

Protected Methods and Ruby 2.0

TL;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

« go back