During a recent code review at work, I was surprised by how Ruby handles private
and protected
method visibility modifiers in derived classes. The behavior is distinct from visibility modifiers in other languages like Java. Specifically, you can call a private parent class method as is demonstrated by the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
I did some experimentation in the irb console. As expected, one may not call the private
method foo on an instance of A:
1 2 3 4 |
|
What surprised me is that in the child class B
that we can call foo
on the parent class. Again, back to irb:
1 2 3 |
|
Class B is calling a private method on class A, which seems to break with my expectations about encapsulation.
Time for some digging. I read through a couple of articles and found a draft of the Ruby language specification. Section 13.3.5 discusses method visibility. While not an official spec, here is what it says about private methods:
A private method cannot be invoked with an explicit receiver, i.e., a private method cannot be invoked if a primary-expression or a chained-method-invocation occurs at the position which corresponds to the method receiver in the method invocation, except for a method invocation of any of the following forms where the primary-expression is a self-expression.
- single-method-assignment
- abbreviated-method-assignment
- single-indexing-assignment
- abbreviated-indexing-assignment
The way I interpret the spec is that a private method can be called with an implicit receiver (read object). This is why calling foo
from method bar
above is allowable. Invoking a method with an explicit receiver is not allowed. This is why A.new.foo
results in a NoMethodError
.
The following code may clarify the difference between calling a method with an implicit vs. explicit receiver:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Note that now we see that the protected
method can be called explicitly, while the private
method cannot. Let’s exercise the above code in irb:
1 2 3 4 5 6 7 8 |
|
References: