Differentiating Private and Protected Methods in Ruby

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 methods
  • protected methods act like private methods when called outside of the class’ definition, but inside the class’ definition they are available for use by other instance methods
  • protected 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 and private instance methods.
  • When using a setter method, you must use an explicit self caller

Time to encapsulate!