Introduction
Ruby provides three levels of method visibility: public, private, and protected. These control who can call a method and from where. Getting visibility right is important for encapsulation โ hiding implementation details and exposing only what’s necessary.
public Methods
public is the default visibility. Public methods can be called by anyone โ from inside the class, from subclasses, or from external code.
class BankAccount
def balance
@balance
end
def deposit(amount)
@balance += amount
end
end
account = BankAccount.new
account.deposit(100) # OK โ public method
puts account.balance # OK โ public method
You can also explicitly mark methods as public, though it’s rarely necessary:
class BankAccount
public
def balance
@balance
end
end
private Methods
private methods can only be called from within the class itself, and only without an explicit receiver (you cannot write self.private_method or object.private_method).
class BankAccount
def initialize(balance)
@balance = balance
end
def withdraw(amount)
if sufficient_funds?(amount)
@balance -= amount
puts "Withdrew #{amount}. New balance: #{@balance}"
else
puts "Insufficient funds."
end
end
private
def sufficient_funds?(amount)
@balance >= amount
end
end
account = BankAccount.new(100)
account.withdraw(50) # => Withdrew 50. New balance: 50
account.sufficient_funds?(10) # => NoMethodError: private method called
private with Explicit Receiver (Ruby 2.7+)
Since Ruby 2.7, you can call private methods with an explicit self. receiver inside the class:
class Foo
def run
self.helper # OK in Ruby 2.7+
helper # always OK
end
private
def helper
"helping"
end
end
But you still cannot call private methods from outside the object.
Defining private Methods Inline
Ruby 2.1+ allows the private keyword directly before a method definition:
class User
def greet
"Hello, #{formatted_name}"
end
private def formatted_name
"#{@first_name} #{@last_name}".strip
end
end
protected Methods
protected methods can be called from within the class and by instances of the same class (or subclasses). They are not accessible from outside.
The key use case is comparing two instances of the same class:
class Employee
def initialize(name, salary)
@name = name
@salary = salary
end
def higher_paid_than?(other)
salary > other.salary # calling protected method on another instance
end
protected
def salary
@salary
end
end
alice = Employee.new("Alice", 80_000)
bob = Employee.new("Bob", 70_000)
puts alice.higher_paid_than?(bob) # => true
puts alice.salary # => NoMethodError: protected method called
Without protected, you’d have to make salary public (exposing it to everyone) or use instance variables directly (breaking encapsulation).
Visibility in Inheritance
Subclasses inherit visibility rules:
class Animal
def speak
sound # calls private method
end
private
def sound
"..."
end
end
class Dog < Animal
private
def sound
"Woof"
end
end
puts Dog.new.speak # => "Woof"
puts Dog.new.sound # => NoMethodError: private method
Protected methods are accessible in subclasses too:
class Shape
def larger_than?(other)
area > other.area
end
protected
def area
0
end
end
class Circle < Shape
def initialize(radius)
@radius = radius
end
protected
def area
Math::PI * @radius ** 2
end
end
small = Circle.new(3)
large = Circle.new(5)
puts large.larger_than?(small) # => true
Changing Visibility
You can change the visibility of inherited or existing methods:
class MyString < String
# Make a public method private
private :upcase
# Make a private method public
public :freeze
end
s = MyString.new("hello")
s.upcase # => NoMethodError: private method
Comparison Table
| Feature | public | private | protected |
|---|---|---|---|
| Called from outside the class | Yes | No | No |
| Called from within the class | Yes | Yes | Yes |
| Called on another instance of same class | Yes | No | Yes |
| Inherited by subclasses | Yes | Yes | Yes |
| Default visibility | Yes | No | No |
Practical Guidelines
- Default to private for helper methods that support public methods
- Use protected when you need to compare or share state between instances of the same class
- Keep public interfaces small โ expose only what callers need
- Avoid making everything public โ it makes refactoring harder and leaks implementation details
class Order
def initialize(items)
@items = items
end
# Public interface
def total
subtotal + tax
end
def summary
"Order total: $#{total.round(2)}"
end
private
# Implementation details โ free to change without breaking callers
def subtotal
@items.sum { |item| item[:price] * item[:quantity] }
end
def tax
subtotal * 0.08
end
end
order = Order.new([
{ price: 10.0, quantity: 2 },
{ price: 5.0, quantity: 3 }
])
puts order.summary # => Order total: $37.7
puts order.subtotal # => NoMethodError: private method
Comments