Overview
Ruby implements access control through method visibility modifiers that restrict how and where methods can be invoked. The three visibility levels - public, private, and protected - control method access based on the calling context and object relationships.
Public methods form the external interface of an object and can be called by any code with a reference to the object. Private methods can only be called by the same object without an explicit receiver, while protected methods can be called by instances of the same class or its subclasses.
class Account
def initialize(balance)
@balance = balance
end
def deposit(amount)
@balance += amount
end
def withdraw(amount)
return false if insufficient_funds?(amount)
@balance -= amount
true
end
protected
def balance
@balance
end
private
def insufficient_funds?(amount)
@balance < amount
end
end
Ruby evaluates access control at method call time, checking the relationship between the caller and the object receiving the method call. The visibility rules apply to both instance methods and class methods, though class methods follow different calling patterns.
Access control interacts with Ruby's object model, inheritance hierarchy, and module inclusion. When classes inherit from other classes or include modules, visibility settings affect which methods remain accessible and how they can be overridden or extended.
Basic Usage
Method visibility is set using the public
, private
, and protected
keywords. These keywords can be used in several ways: as method calls that affect subsequent method definitions, as method calls with specific method names as arguments, or as blocks that contain method definitions.
class User
def name
@name
end
private
def validate_email
# Private method
end
def encrypt_password
# Also private
end
public
def email
@email
end
protected
def internal_id
@internal_id
end
end
The most common pattern places visibility modifiers before groups of method definitions. All methods defined after a visibility modifier inherit that visibility until another modifier is encountered or the class definition ends.
class BankAccount
def initialize(owner)
@owner = owner
@balance = 0
end
def deposit(amount)
validate_amount(amount)
@balance += amount
end
def current_balance
format_currency(@balance)
end
private
def validate_amount(amount)
raise ArgumentError, "Amount must be positive" unless amount > 0
end
def format_currency(amount)
"$#{'%.2f' % amount}"
end
end
Visibility can also be set for specific methods by passing method names as symbols to the visibility keywords. This approach is useful when you need to change visibility after method definition or when working with methods defined through metaprogramming.
class Configuration
def database_url
@database_url
end
def api_key
@api_key
end
def secret_token
@secret_token
end
private :api_key, :secret_token
end
Protected methods enable controlled access between objects of the same class. Unlike private methods, protected methods can be called with an explicit receiver, but only when the receiver is an instance of the same class or a subclass.
class Person
def initialize(age)
@age = age
end
def older_than?(other_person)
age > other_person.age
end
protected
def age
@age
end
end
person1 = Person.new(25)
person2 = Person.new(30)
person1.older_than?(person2) # Works - protected method called on same class
Advanced Usage
Access control integrates deeply with Ruby's metaprogramming capabilities. Methods defined through define_method
, class_eval
, and other metaprogramming techniques inherit the current visibility setting at the time of definition.
class DynamicAccess
%w[read write execute].each do |permission|
define_method("can_#{permission}?") do
instance_variable_get("@#{permission}_permission")
end
end
private
%w[validate reset].each do |action|
define_method("#{action}_permissions") do
# These methods are private
permissions = instance_variables.select { |var| var.to_s.end_with?('_permission') }
permissions.map { |var| instance_variable_get(var) }
end
end
end
Module inclusion affects visibility in complex ways. When a module is included, its methods become instance methods of the including class with their original visibility. However, methods defined in the including class can override module methods and change their visibility.
module Auditable
def log_action(action)
timestamp = Time.now
write_to_audit_log(timestamp, action)
end
private
def write_to_audit_log(timestamp, action)
puts "#{timestamp}: #{action}"
end
end
class Document
include Auditable
def update_content(content)
@content = content
log_action("content updated")
end
# Override module method with different visibility
public :write_to_audit_log
end
Class methods follow similar visibility rules but are defined and called differently. Class method visibility is controlled using private_class_method
or by defining methods within class << self
blocks with appropriate visibility modifiers.
class DatabaseConnection
class << self
def connect(config)
validate_config(config)
establish_connection(config)
end
private
def validate_config(config)
raise "Invalid config" unless config.is_a?(Hash)
end
def establish_connection(config)
# Connection logic
end
end
# Alternative syntax for class method visibility
def self.disconnect
# Public class method
end
def self.reset_pool
# Will be made private
end
private_class_method :reset_pool
end
Inheritance creates visibility hierarchies where subclasses can call protected methods of their parent classes but cannot reduce the visibility of inherited public methods. However, private methods are not directly accessible to subclasses, though they can be overridden.
class Vehicle
def start_engine
perform_safety_checks
ignite_engine
end
protected
def perform_safety_checks
check_fuel_level
check_oil_pressure
end
private
def ignite_engine
puts "Engine started"
end
def check_fuel_level
puts "Fuel OK"
end
def check_oil_pressure
puts "Oil pressure OK"
end
end
class Car < Vehicle
def diagnostic_report
perform_safety_checks # Can call protected parent method
# ignite_engine # Cannot call private parent method directly
end
private
def ignite_engine
puts "Car engine started with keyless ignition"
super # But can override and call super
end
end
Common Pitfalls
Private method behavior confuses many developers because private methods cannot be called with an explicit receiver, even self
. This restriction applies regardless of the calling context, leading to unexpected NoMethodError
exceptions.
class Calculator
def compute
value = calculate_result
self.format_result(value) # NoMethodError - private method with explicit receiver
end
private
def calculate_result
42
end
def format_result(value)
"Result: #{value}"
end
end
# Fix: call private methods without receiver
class Calculator
def compute
value = calculate_result
format_result(value) # Works - no explicit receiver
end
private
def calculate_result
42
end
def format_result(value)
"Result: #{value}"
end
end
Protected method semantics are frequently misunderstood. Protected methods can be called with an explicit receiver, but only when the receiver is an instance of the same class or a subclass. This creates subtle bugs when working with object hierarchies.
class Employee
def initialize(salary)
@salary = salary
end
def compare_salary(other)
salary <=> other.salary # Works - same class
end
protected
def salary
@salary
end
end
class Manager < Employee
def compare_with_employee(employee)
salary <=> employee.salary # Works - employee is superclass instance
end
end
class Customer
def initialize(spending)
@spending = spending
end
def compare_with_employee(employee)
@spending <=> employee.salary # NoMethodError - different class hierarchy
end
end
Visibility changes during class reopening can create confusing situations where methods appear to be public but are actually private due to the visibility context when the class was reopened.
class User
def name
@name
end
private
def email
@email
end
end
class User # Reopening class
def phone
@phone # This method is private! Current visibility is private
end
public # Reset visibility
def address
@address
end
end
user = User.new
user.phone # NoMethodError - method is private
Method visibility interacts unpredictably with method_missing
. When a private method exists but is called with an explicit receiver, Ruby bypasses method_missing
and raises NoMethodError
directly.
class Proxy
def method_missing(method_name, *args)
puts "Method #{method_name} was called with #{args}"
end
private
def hidden_method
"secret"
end
end
proxy = Proxy.new
proxy.nonexistent_method # Calls method_missing
proxy.hidden_method # NoMethodError - method_missing not called
Access control checks happen at call time, not definition time. This means methods can change behavior based on when visibility modifiers are applied, particularly when using metaprogramming or dynamic method definition.
class DynamicClass
def self.create_method(name, visibility)
define_method(name) do
"Method #{name}"
end
send(visibility, name)
end
create_method(:public_method, :public)
create_method(:private_method, :private)
# Later in the class...
public :private_method # Changes visibility after creation
end
obj = DynamicClass.new
obj.private_method # Works - method is now public
Singleton methods (methods defined on individual objects) do not inherit class-level visibility settings and are always public by default. This behavior surprises developers who expect instance-specific methods to follow class visibility rules.
class Document
private
def internal_method
"private"
end
end
doc = Document.new
def doc.special_method
internal_method # Can call private method - singleton methods bypass visibility
end
doc.special_method # Works, even though internal_method is private
Reference
Visibility Modifiers
Modifier | Scope | Receiver Rules | Inheritance |
---|---|---|---|
public |
Global access | Any receiver allowed | Inherited as public |
private |
Same object only | No explicit receiver | Inherited as private |
protected |
Same class hierarchy | Same class/subclass receiver only | Inherited as protected |
Visibility Control Methods
Method | Parameters | Returns | Description |
---|---|---|---|
public(*method_names) |
*method_names (Symbol) |
self |
Makes specified methods public |
private(*method_names) |
*method_names (Symbol) |
self |
Makes specified methods private |
protected(*method_names) |
*method_names (Symbol) |
self |
Makes specified methods protected |
private_class_method(*method_names) |
*method_names (Symbol) |
self |
Makes class methods private |
public_class_method(*method_names) |
*method_names (Symbol) |
self |
Makes class methods public |
Visibility Query Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#private_methods(include_super=true) |
include_super (Boolean) |
Array<Symbol> |
Returns private method names |
#protected_methods(include_super=true) |
include_super (Boolean) |
Array<Symbol> |
Returns protected method names |
#public_methods(include_super=true) |
include_super (Boolean) |
Array<Symbol> |
Returns public method names |
.private_instance_methods(include_super=true) |
include_super (Boolean) |
Array<Symbol> |
Returns class private methods |
.protected_instance_methods(include_super=true) |
include_super (Boolean) |
Array<Symbol> |
Returns class protected methods |
.public_instance_methods(include_super=true) |
include_super (Boolean) |
Array<Symbol> |
Returns class public methods |
Access Control Patterns
Pattern | Usage | Example |
---|---|---|
Group visibility | Apply to multiple methods | private def method1; end; def method2; end |
Selective visibility | Apply to specific methods | private :method1, :method2 |
Block visibility | Contain method definitions | private do; def method; end; end |
Class method visibility | Control class method access | private_class_method :helper |
Visibility restoration | Reset visibility level | public (with no arguments) |
Inheritance Visibility Rules
Scenario | Parent Visibility | Child Override | Result Visibility |
---|---|---|---|
Override public | public |
Any | Maintains public |
Override protected | protected |
public/protected |
As specified |
Override private | private |
Any | As specified in child |
No override | Any | N/A | Inherits parent visibility |
Common Error Types
Error | Cause | Solution |
---|---|---|
NoMethodError: private method |
Calling private method with receiver | Remove explicit receiver |
NoMethodError: protected method |
Calling protected method from wrong class | Ensure same class hierarchy |
ArgumentError: wrong number of arguments |
Incorrect visibility modifier usage | Check method signature |
Metaprogramming Visibility Effects
Method | Visibility Inheritance | Notes |
---|---|---|
define_method |
Current context visibility | Inherits visibility at definition time |
alias_method |
Original method visibility | Maintains original visibility |
module_function |
Creates public module method, private instance method | Special dual visibility |
class_eval |
Context visibility | Methods inherit evaluation context |
instance_eval |
Singleton method (public) | Creates public singleton methods |