Jun 03, 2015
Table of contents:
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.
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.
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.
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.
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
.
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.
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.