Surprising Behavior With Private Methods In Ruby
Private methods in Ruby are pretty straightforward, but there are a few gotchas to be aware of.
Private Instance Methods
Private instance methods work pretty much as you would expect:
class Person
def salutation
"Hello from #{name}"
end
private
def name
"Bob"
end
end
person = Person.new
person.salutation # Hello from Bob
person.name # NoMethodError: private method `name' called
Instead of using the private
keyword by itself - in which case all methods following this line will be private - you can also selectively declare private methods.
class Person
def full_name
"#{first_name} #{last_name}"
end
def first_name
"Roger"
end
def last_name
"Wilco"
end
private :first_name, :last_name
end
person = Person.new
person.full_name # Roger Wilco
person.first_name # NoMethodError: private method `first_name' called
Private Class Methods
Class methods don’t necessarily work the same way:
class Person
def self.first
people.first
end
private
def self.people
["Roger Wilco", "Sonny Bonds", "King Graham"]
end
end
Person.first # "Roger Wilco"
Person.people.count # 3
What’s going on here? The private
keyword is actually a method call on the instance’s class which sets the visibility for subsequently defined methods. It doesn’t affect subsequent class method declarations, as you can see from the result. If you’re interested in understanding why this is, I would suggest you read this excellent post by Jake Jesbeck. The short answer is that it’s due to the way Ruby looks up method declarations, known as dynamic dispatch.
You have 3 options here - you can either explicitly declare a private class method:
class Person
def self.first
people.first
end
def self.people
["Roger Wilco", "Sonny Bonds", "King Graham"]
end
private_class_method :people
end
Person.first # "Roger Wilco"
Person.people.count # NoMethodError: private method `people' called
Or you can use the alternate syntax for declaring class methods:
class Person
class << self
def first
people.first
end
private
def people
["Roger Wilco", "Sonny Bonds", "King Graham"]
end
end
end
Person.first # "Roger Wilco"
Person.people.count # NoMethodError: private method `people' called
A third option (which I haven’t really seen used anywhere, but it’s an option) is to use a module
.
class Person
module ClassMethods
def first
people.first
end
private
def people
["Roger Wilco", "Sonny Bonds", "King Graham"]
end
end
extend ClassMethods
end
Person.first # "Roger Wilco"
Person.people.count # NoMethodError: private method `people' called
Private Initializers
One more gotcha is with private initializers:
class Person
def self.build(full_name)
first_name, last_name = full_name.split
new(first_name, last_name)
end
attr_reader :first_name, :last_name
private
def initialize(first_name, last_name)
@first_name, @last_name = first_name, last_name
end
end
roger = Person.build("Roger Wilco")
roger.first_name # Roger
sonny = Person.new("Sonny", "Bonds")
sonny.first_name
What’s going on here? If initialize
is private, why can we still create a new object from outside of the class? This is because new
and initialize
are 2 different methods - in fact, the initialize
method is always private!
class Person
def initialize
puts "Initializing..."
end
end
person = Person.new # Initializing...
person.initialize # NoMethodError: private method `initialize' called
This is a topic for a longer discussion, but since we are really calling new
(which ends up calling initialize
) we need to set new
to private.
class Person
private_class_method :new
def self.build(full_name)
first_name, last_name = full_name.split
new(first_name, last_name)
end
attr_reader :first_name, :last_name
def initialize(first_name, last_name)
@first_name, @last_name = first_name, last_name
end
end
roger = Person.build("Roger Wilco")
roger.first_name # Roger
sonny = Person.new("Sonny Bonds") # private method `new' called for Person:Class
Private Methods in Subclasses
When you override a method the public/private level of the subclass wins out:
class User
def name
"Bob"
end
private
def id
5
end
end
class Person < User
def id
6
end
private
def name
"Steve"
end
end
user = User.new
user.name # Bob
user.id # private method `id' called
person = Person.new
person.id # 6
person.name # private method `name' called
This doesn’t apply to the initialize
method though, which is always private.