Private and Protected: They Might Not Mean What You Think They Mean
Ruby1, like many other languages, provides a built-in way to change method visibility. Used properly it can help create a roadmap for others developers to follow. The problem is that markers of the same name can vary in meaning between languages. If you’ve taken up Ruby after learning another language like C++ or Java and you haven’t reexamined the meaning of “private” and “protected” then you’re almost certainly not using them the right way. Let’s spend a minute investigating their use in Ruby.
Private
In Ruby marking a method as private
means that it can’t have an explicit receiver.
Below we define a Person
class that takes a first name, last name, and an age.
People don’t usually mind telling you their name but some are squeamish about revealing their age.
class Person
attr_reader :first_name, :last_name
def initialize(first_name, last_name, age)
@first_name = first_name
@last_name = last_name
@age = age
end
def name
[first_name, last_name].join(' ')
end
private
attr_reader :age
end
Let’s create a new Person
and ask them their name.
> john = Person.new('John', 'Doe', 31)
# => <Person:0x110800cf8 @age=31, @last_name="Doe", @first_name="John">
> john.name
# => "John Doe"
What happens if we ask about their age?
> john.age
# => NoMethodError: private method `age' called for #<Person:0x110800cf8>
We get an error because age
is called with the explicit receiver john
.
We can still access age
we just have to do it inside of Person
and without an explicit receiver.
Let’s explore this by equiping Person
with a birthday!
method.
def birthday!
age += 1
self
end
private
attr_accessor :age
Now we can call birthday!
to update their age.
> john.birthday!
# => NoMethodError: undefined method `+' for nil:NilClass
Ruby thinks age
is a local variable and it’s defaulting to nil
.
When using a setter in Ruby we’re supposed to use an explicit receiver like self
.
Here we have a private method that doesn’t allow an explicit receiver.
We appear to have reached an impasse.
In this case it turns out Ruby breaks its own rule.
def birthday!
self.age += 1
self
end
Now we’ve got it.
> john.birthday!
# => NoMethodError: private method `age' called for #<Person:0x110800cf8>
Ugh.
We’ve used +=
which means that self.age
is being called as a getter and a setter.
The setter needs an explicit receiver but the getter can’t have one.
def birthday!
self.age = age + 1
self
end
One more time.
> john.birthday!
# => #<Person:0x110800cf8 @age=32, @last_name="Doe", @first_name="John">
Hurrah! Let’s see the final class.
class Person
attr_reader :first_name, :last_name
def initialize(first_name, last_name, age)
@first_name = first_name
@last_name = last_name
@age = age
end
def name
[first_name, last_name].join(' ')
end
def birthday!
self.age = age + 1
self
end
private
attr_accessor :age
end
What if we want to inherit from Person
?
Those private
methods will be available in the child class just like they are in the parent.
Remember, Ruby’s only concern is how the method is called, not who’s receiving the call.
Protected
Like private
, protected
also concerns itself with the receiver.
Methods marked with protected
can only be called using self
, an instance of the same class as self
, or an instance of a subclass of the class of self
.
Who’s up for ping pong?
class AbstractPingPongPlayer
attr_reader :name
def initialize(name)
@name = name
end
def play(opponent)
# Here opponent is a different instance so if
# skill_level was private instead of protected
# it wouldn't be accessible.
winner = if opponent.skill_level > skill_level
opponent
else
self
end
puts "#{winner.name} wins!"
end
protected
attr_reader :skill_level
end
class AmateurPingPongPlayer < AbstractPingPongPlayer
def initialize(*args)
super
@skill_level = rand(50)
end
end
class ProPingPongPlayer < AbstractPingPongPlayer
def initialize(*args)
super
@skill_level = rand(50) + 50
end
end
First we’ll need two players.
> jane = ProPingPongPlayer.new('Jane')
# => #<ProPingPongPlayer:0x007fe7cb8dadf0 @name="Jane", @skill_level=95>
> susan = AmateurPingPongPlayer.new('Susan')
# => #<AmateurPingPongPlayer:0x007fe7cb8ac748 @name="Susan", @skill_level=30>
If you ask Jane how skilled she is you’re not going to get an answer.
> jane.skill_level
# => NoMethodError: protected method `skill_level' called for #<ProPingPongPlayer:0x007fe7cb8dadf0>`
Play her and you’ll quickly find out.
> susan.play(jane)
# => "Jane wins!"
Rule of Thumb
If you’re looking to remove something from your public interface then you’re looking for private
.
If you’re looking to narrow the interface to other objects of the same type then you want protected
.
-
Version 1.9.3 ↩