Part of the way Ruby uses encapsulation is to provide ways to hide an object’s behaviors from use by the public. This can be very useful to help prevent unwanted changes to a program. While instance methods are by default public, it is good to have very few public methods available to the public interface.
Public methods are easy enough to understand, but I struggled for a long time to understand the difference between protected and private methods. Here’s my take on differentiating these two concepts.
Private Methods
In our example below, we have a Student
class definition from which we can instantiate an object that has attributes @grade
and @gpa
stored in instance variables within the state of the object. We have called the attr_reader
method and passed in the symbols :grade
and :gpa
to it as an argument, which means we now have access to the getter methods that will expose the values referenced by these instance variables. We also have access to these getter methods inside of our other instance method definition called Student#print_transcript
.
class Student
attr_reader :grade, :gpa
def initialize(grade, gpa)
@grade = grade
@gpa = gpa
end
def print_transcript
puts "Grade: #{grade}. GPA: #{gpa}."
end
end
student = Student.new(10, 3.5)
p student.gpa # => 3.5
However, let’s say for privacy reasons, we only want these getter methods to be accessible from within the #print_transcript
instance method. All instance methods are by default public, but we can call private
in the class definition and anything below that method invocation will become unavailable to the public interface, as we can see in line 18 below: when we try to call #gpa
on the student
object, a NoMethodError
is raised.
However, these private getter methods are still available to be invoked within other instance method definitions. In the code below, we can see that in the Student#print_transcript
method, the #grade
and #gpa
private methods are invoked and their return values are used in string interpolation. (As you can see on line 17, the output is “Grade: 10. GPA: 3.5.” and the return value is nil
because of the puts
method, which passed in this string interpolation to it as an argument.)
class Student
def initialize(grade, gpa)
@grade = grade
@gpa = gpa
end
def print_transcript
puts "Grade: #{grade}. GPA: #{gpa}."
end
private
attr_reader :grade, :gpa
end
student = Student.new(10, 3.5)
student.print_transcript # => output: Grade: 10. GPA: 3.5. returns: nil
student.gpa # => NoMethodError: private method `gpa' called for #<Student:0x00005654c06faea8 @grade=10, @gpa=3.5>
Protected Methods
Let’s say we want to be able to get some values from one object and compare them to those of another object within an instance method, but still maintain a level of data protection outside of the class definition. The protected
method invocation allows us to access instance methods of other objects from that same class. protected
methods are accessible from inside the class just like public methods but outside the class protected
methods act like private
methods and are unavailable.
In the example below, we have two different class definitions: Student
and Teacher
. Both classes have an instance variable called @gpa
and both classes have an attr_reader
getter method invocation with :gpa
passed in as an argument. If we were to call #gpa
on either a Student
or a Teacher
object, we would get a NoMethodError: protected method gpa called
because protected
methods act like private
methods when called outside of the class definition.
For our example below, we have instantiated two different Student
objects and one Teacher
object. When we call #compare_gpa
on our student
object and pass in student2
object to it as an argument, within the class definition and within the #compare_gpa
instance method, student2
is passed in as an argument. #gpa
on line 7 is called with an implicit caller, meaning it is calling the getter instance method #gpa
without needing to use the explicit caller self
because it understands that it is referring to itself. The return value of this method call has a method called on it: the #>
method, with an argument passed in consisting of the return value of #gpa
called on the object passed in as an argument. This returns a boolean true
or a boolean false
, which we can see in line 28. This is possible even though the #gpa
getter method is protected because both objects are instances of the same class, proving that protected
methods are available to other objects of the same class within the class definition.
class Student
def initialize(gpa)
@gpa = gpa
end
def compare_gpa(other)
gpa > other.gpa
end
protected
attr_reader :gpa
end
class Teacher
def initialize(gpa)
@gpa = gpa
end
protected
attr_reader :gpa
end
student = Student.new(3.5)
student2 = Student.new(1.0)
teacher = Teacher.new(4.0)
p student.compare_gpa(student2) # => true
p student.compare_gpa(teacher) # => NoMethodError
However, when we try to compare a Student
object with a Teacher
object, we see that there is a NoMethodError: protected method gpa called for #<Teacher:
. Because the teacher
object has a protected
getter method, it is not accessible outside of its own class definition, proving the second fact about protected
methods that outside of their class definition they act like private
methods.
It’s possible to use a combination of protected
and private
instance methods for further customization:
class Student
def initialize(grade, gpa)
@grade = grade
@gpa = gpa
end
def compare_class_rank(other)
if grade == other.grade
gpa > other.gpa ? "higher ranked" : "lower ranked"
else
"Not in the same grade!"
end
end
def promote_to_next_grade
self.grade += 1
end
protected
attr_reader :grade, :gpa
private
attr_writer :grade
end
student = Student.new(10, 3.5)
student2 = Student.new(10, 3.0)
student3 = Student.new(11, 4.0)
p student.compare_class_rank(student2) # => "higher ranked"
p student2.compare_class_rank(student3) # => "Not in the same grade!"
student.promote_to_next_grade
p student.compare_class_rank(student3) # => "lower ranked"
Of course, this code is missing a few instance method details, like what happens if the students have the same GPA. But the principle is here: we have access to the setter method #grade=
even though it is private because it is possible to use private
methods within other instance method definitions of the same class. The protected
methods retain their same functionality. One further thing to note with the example above is that the setter method for #gpa=
is not available anywhere, further encapsulating this piece of data from being changed.
Using an Explicit Caller
When calling a protected or private setter method inside a public instance method, it is important to use an explicit self
caller. self
refers to the object itself, and then you can call the protected or private instance method on this explicit self
caller. This is necessary because otherwise Ruby does not identify the setter method as a setter method but rather a local variable assignment.
In the code below, our Student
class is defined with one instance variable @gpa
and has an attr_reader
getter method. It also has a attr_writer
setter method, but this method is defined below the private
method invokation, meaning it is only available to be used in other instance method definitions.
class Student
attr_reader :gpa
def initialize(gpa)
@gpa = gpa
end
def change_gpa(new_gpa)
self.gpa = new_gpa
end
private
attr_writer :gpa
end
student = Student.new(3.5)
p student.change_gpa(2.5)
p student.gpa # => 2.5
The instance method change_gpa
takes an argument and calls the private setter method in order to change the value of @gpa
. As you can see, we have to call the setter using an explicit self
. When we call #gpa
using the getter method on our student
object, we can see that the GPA has been changed from a Float
object with a value of 3.5
to a Float
object with a value of 2.5
.
Conclusions
To summarize:
- Instance methods are public by default
private
methods are only available to be called within the object’s own instance methodsprotected
methods act likeprivate
methods when called outside of the class’ definition, but inside the class’ definition they are available for use by other instance methodsprotected
methods are available for use in the instance methods of another object of the same class- It’s possible to use a combination of
protected
andprivate
instance methods. - When using a setter method, you must use an explicit
self
caller
Time to encapsulate!