cult3

The difference between Public, Protected and Private methods in Ruby

Jun 03, 2015

Table of contents:

  1. What do Public, Protected and Private mean?
  2. Why is this distinction important?
  3. Understanding Public methods
  4. Private methods
  5. Protected Methods
  6. Conclusion

In last week’s tutorial, we looked at the basics of defining a new class in Ruby.

Ruby is an object-oriented programming language and so defining classes is something you will find yourself doing a lot as a Ruby programmer.

Creating good Ruby objects is as much of an art as a science. When we plan and create objects in object-oriented programming, we talk about it as design.

There really is a beauty in well designed objects. A well designed object will have a number of characteristics and qualities that not only make it easy to work with, but it will also make it easy to evolve and coexist within the boundaries of a bigger application.

In today’s tutorial we’re going to be looking at one such important aspect of designing Ruby objects in the differences between Public, Protected and Private methods.

What do Public, Protected and Private mean?

Public, Protected and Private refer to the accessibility of those methods.

By default, all methods are Public. If you do not specify the accessibility of a method, it will be Public.

So far in this series we’ve seen a couple of examples of public methods. This means that when you have an instance of an object, you can call it’s Public methods.

Protected and Private methods are not publicly accessible and so when you have an instance of an Object you will not be able to call those methods.

Protected and Private methods also have differences around how you can use them within the context of an Object.

This can seem very abstract without looking at code so that’s exactly what we’ll do.

Why is this distinction important?

A big part of writing good objects is designing the interface. When we talk about the interface of an object, we mean the publicly accessible methods.

Part of this is writing methods that are well named, have clear actions and no weird side effects or coupling. If you don’t know what’s going to happen when you invoke a method, it’s probably a sign of bad design.

Another part of good object interface design is the separation of public, protected and private methods.

The Public methods of an object say a lot about it’s responsibility, behaviour and role within the application.

Protected and Private methods are important to the object’s internal implementation, but they are not the concern of the outside world.

Drawing this line and keeping a clean and clear public API is one of the most important aspects of good object-oriented design.

Understanding Public methods

Public methods are by far the easiest to understand and so we’ll start this little exploration from that vantage point.

Imagine we have this class:

class Product
  attr_accessor :name

  def initialize(name)
    @name = name
  end
end

This is a simple Product class that accepts a name on instantiation.

We are also using the attr_accessor method we saw last week to automatically create the getter and setter methods for the name property.

When we call the name method on the object, we should be returned the product’s name:

milk = Product.new('Milk')
# => #<Product:0x007fd7a182f070 @name="Milk">

milk.name
# => "Milk"

Remember, when we use attr_accessor it means Ruby is simply automatically creating the following methods, so we don’t have to write them out:

def name
  @name
end

def name=(name)
  @name = name
end

This is a Public method and so you can call it from outside the scope of the object.

Public methods should describe the behaviour of the object and should allow other objects to send it messages.

For example, the product might also have a quantity property. In order to increase the quantity, we can add an increment method:

class Product
  attr_accessor :name, :quantity

  def initialize(name)
    @name = name
    @quantity = 1
  end

  def increment
    @quantity += 1
  end
end

Now the increment method is available as part of the object’s public API. We can call this method like this:

milk = Product.new('Milk')
# => #<Product:0x007fd7a231e648 @name="Milk", @quantity=1>

milk.quantity
# => 1

milk.increment
# => 2

milk.quantity
# => 2

As you can see, incrementing the quantity of a Product is an important behaviour of the object and so it makes sense to have it as a publicly accessible method.

Private methods

Both Private and Protected methods are not accessible from outside of the object as they are used internally to the object.

Another important aspect of good object-oriented design is that the consumer of an object should not have to know how that object is implemented.

Private methods and Protected methods, whilst both being inaccessible outside of the scope of the object, have a subtle difference.

I think it’s easier to understand Private methods, and so we’ll start here.

To define a private method you use the private keyword. private is not actually a keyword, it’s a method, but for all intents and purposes, it’s easier to just think of it as a keyword.

For example, we might have the following method in our Product class:

class Product
  attr_accessor :name, :quantity

  def initialize(name)
    @name = name
    @quantity = 1
  end

  def increment
    @quantity += 1
  end

  private

  def stock_count
    100
  end
end

To signify that the stock_count method is private we can place it under the private heading.

There is actually a couple of different ways to define private methods, but I think the method above is the most common.

Now when we have an instance of the Product object, we can’t call the stock_count method because it is private:

milk = Product.new('Milk')
# => #<Product:0x007fd7a22b53f0 @name="Milk", @quantity=1>

milk.stock_count
# NoMethodError: private method 'stock_count' called for #<Product:0x007fd7a22b53f0 @name="Milk", @quantity=1>

In order to call the stock_count method, you need to be within the scope of the object:

class Product
  attr_accessor :name, :quantity

  def initialize(name)
    @name = name
    @quantity = 1

    puts "There are #{stock_count} in stock"
  end

  def increment
    @quantity += 1
  end

  private

  def stock_count
    100
  end
end

Now when we instantiate a new instance of the object, we will see the stock count:

milk = Product.new('Milk')
# There are 100 in stock
# => #<Product:0x007fd7a22909d8 @name="Milk", @quantity=1>

So the only way to call a Private method is to do so within the context of the object instance.

However, an interesting thing to note about Private Ruby methods is the fact that a Private method cannot be called with an explicit receiver, even if that receiver is itself.

When I say “receiver”, I mean the object that the method is being called from.

So for example:

class Product
  attr_accessor :name, :quantity

  def initialize(name)
    @name = name
    @quantity = 1

    puts "There are #{self.stock_count} in stock"
  end

  private

  def stock_count
    100
  end
end

In this example I’ve modified the initialize method to use self.stock_count. In this case, self refers to the current object.

However, when you attempt to create a new instance of Product you will get an error:

milk = Product.new('milk')
# NoMethodError: private method `stock_count' called for #<Product:0x007fd7a2279f08 @name="milk", @quantity=1>

So you can only call Private methods from the current context of the object, and you can’t call Private methods with a receiver, even if that receiver is self.

Protected Methods

Finally we have Protected methods. A Protected method is not accessible from outside of the context of the object, but it is accessible from inside the context of another object of the same type.

For example, imagine we have the following sku method on the Product class:

class Product
  attr_accessor :name, :quantity

  def initialize(name)
    @name = name
    @quantity = 1

    puts "The SKU is #{sku}"
  end

  protected

  def sku
    name.crypt('yo')
  end
end

In this example we are generating a SKU from the product’s name.

As with the Private method example from earlier, this method is not accessible outside the context of the object:

milk = Product.new('Milk')
# The SKU is yo.B6xygWtQ1w
# => #<Product:0x007fd7a2184058 @name="Milk", @quantity=1>

milk.sku
# NoMethodError: protected method 'sku' called for #<Product:0x007fd7a2184058 @name="Milk", @quantity=1>

However, if you call the method with self it will work:

class Product
  attr_accessor :name, :quantity

  def initialize(name)
    @name = name
    @quantity = 1

    puts "The SKU is #{self.sku}"
  end

  protected

  def sku
    name.crypt('yo')
  end
end

So a Protected class method can be called within the context of an object of the same type.

This means you can call Protected class methods of other objects inside an object of the same type. For example:

class Product
  attr_accessor :name, :quantity

  def initialize(name)
    @name = name
    @quantity = 1
  end

  def ==(other)
    self.sku == other.sku
  end

  protected

  def sku
    name.crypt('yo')
  end
end

In this class we’ve implement the == method to assert equality between two objects. This method accepts another Product object and will call the Protected sku method.

If the two Product objects have the same name, they will be considered equal:

milk1 = Product.new('Milk')
# => #<Product:0x007fd7a20a8418 @name="Milk", @quantity=1>

milk2 = Product.new('Milk')
# => #<Product:0x007fd7a2099558 @name="Milk", @quantity=1>

bread = Product.new('Bread')
# => #<Product:0x007fd7a208a440 @name="Bread", @quantity=1>

milk1 == bread
# => false

milk1 == milk2
# yo
# => true

So the important thing to note here is, you can call Protected methods inside the context of an object of the same type.

Conclusion

The differences between Public, Private and Protected methods can seem confusing at first, particularly the differences between Private and Protected methods.

Writing a clean and easy to use public interface for your objects is a very important part of the design process. The public API will say a lot about the object and how it should be used. It’s important to take care and ensure you make good choices when deciding on method names and what methods should be public.

The choice between Private and Protected will come down to how you intend your object to be used.

As with just about everything else in programming, learning via experience is really the only way to progress. If today’s tutorial seemed like a lot to take in, don’t worry about it. As long as you keep exploring, you will eventually find the way.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.