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' calledInstead 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' calledPrivate 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 # 3What’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' calledOr 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' calledA 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' calledPrivate 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_nameWhat’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' calledThis 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:ClassPrivate 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' calledThis doesn’t apply to the initialize method though, which is always private.