[RubyJuice] Public, private, protected methods in ruby

Ruby newbies often get confused when they learn about method access levels. Other languages use the same denomination (notably PHP and Java) but with slightly different meaning, so let’s clarify the use of public, private and protected with some examples.

Public

Let’s start from the basics: by default methods are defined as public, so you don’t need to explicitly type that keyword inside your class:

class SshKeyPair
  def id_rsa_pub
    'I am the public key'
  end
end

pair = SshKeyPair.new
c.id_rsa_pub
# => "I am the public key"

Let’s use ruby introspection to confirm that the method has been added to the object methods list:

pair.methods.include? :id_rsa_pub
# => true

pair.public_methods.include? :id_rsa_pub
# => true

Private

Private methods can be accessed only without receiver, practically this means that only the very same instance can access them, and only when the self receiver is omitted:

class SshKeyPair
  private

  def id_rsa
    'I am the private key'
  end
end

pair.id_rsa
# NoMethodError: private method `id_rsa' called for #<SshKeyPair:0x007f87f139be28>

pair.methods.include? :id_rsa
# => false
pair.private_methods.include? :id_rsa
# => true
pair.protected_methods.include? :id_rsa
# => false

Of course you can always use send to circumvent restrictions (but remember that probably that method was declared private for good reason):

pair.send :id_rsa
# => "I am the private key"

Now let’s see the different behavior of the method when called with and without self directly inside the class:

class SshKeyPair
  def leak_private_key
    id_rsa
  end

  def lousy_leak_private_key
    self.id_rsa
  end
end

pair.leak_private_ley
# => "hello, privately"

pair.lousy_leak_private_key
NoMethodError: private method `id_rsa' called for #<SshKeyPair:0x007f87f139be28>

There’s one exception: writer methods. When declared private, they still can be used inside the class using self:

class Album
  def set_name(name)
    self.name = name
  end

  private

  attr_accessor :name
end

album = Album.new
# => #<Album:0x007fb439125328>
album.set_name 'Pet Sounds'
# => "Pet Sounds"
album.name
NoMethodError: private method `name' called for #<Album:0x007fb439125328 @name="Pet Sounds">
album.send :name
# => "Pet Sounds"
album.name = 'Smile'
NoMethodError: private method `name=' called for #<Album:0x007fb439125328 @name="Pet Sounds">

I think this happens because there is now way to use a writer method without making self explicit.

Protected

protected is probably the most misunderstood in the group: it works like private, but the method is visible also by instances of the same class:

class Woman
  attr_accessor :age

  protected :age
end

kate = Woman.new
kate.age
# NoMethodError: protected method `age' called for #<Woman:0x007fb439112340>

kate.protected_methods.include? :age
# => true
kate.private_methods.include? :age
# => false
kate.methods.include? :age
# => true

This makes sense when instance should be allowed to interact with each other while not exposing their behaviour publicly. Let’s enhance our Woman class so that it allows age comparisons keeping the age secret:


class Woman
  def older?(other)
    (age <=> other.age) == 1
  end
end

mother       = Woman.new
daughter     = Woman.new
mother.age   = 35
daughter.age = 10

mother.older? daughter
# => true

But what happens with subclasses? Will it still work? Of course it does:

class Girl < Woman
  def age=(value)
    raise 'Girls older than 20 years are women!' if value > 20
    @age = value
  end
end

daughter = Girl.new
daughter.age = 10

daughter.older? mother
# => false

Statements can be mixed and repeated freely to suit your tastes:

class C
  private
    def a; end

  public
    def b; end

  protected
    def c; end

  private
    def d; end
  end
end

or you can change the method visibility on second thought:

class C
  def a; end
end

class C
  private :a
end

Ruby 2.1 additions

The recently released Ruby 2.1 introduced a few changes that affect method access level keywords: basically, they’re no more simple keywords but full-fledged methods, so they can be redefined:

class C
  def self.private(*m_names)
    m_names.each {|m_name| puts "#{m_name} is now a private method."}
  end
end

class C
  def a; end

  private :a
end
# a is now a private method.

The second change is that method definitions with def now return the method name symbol, so we can write:

class C
  private def b; end
end
# b is now a private method.