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.