Overview
Access modifiers define the scope and visibility of methods, instance variables, and classes within a program. These language-level controls determine which parts of code can access specific members of a class or module, forming the foundation of encapsulation in object-oriented design.
The concept originated in languages like Simula and Smalltalk, where controlling member visibility became essential for managing complexity in large codebases. Access modifiers enforce boundaries between different parts of a system, preventing unintended interactions and reducing coupling between components.
In Ruby, access modifiers work differently from languages like Java or C++. Ruby provides three primary access levels: public, private, and protected. Unlike statically-typed languages where access control is enforced at compile time, Ruby enforces these restrictions at runtime. This distinction affects how access violations manifest and how developers must think about encapsulation.
class BankAccount
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
private
def insufficient_funds?(amount)
amount > @balance
end
end
account = BankAccount.new(1000)
account.deposit(500)
account.withdraw(200)
# account.insufficient_funds?(100) # => NoMethodError: private method
Access modifiers serve multiple purposes in software design. They hide implementation details, preventing external code from depending on internal mechanisms that might change. They enforce invariants by restricting which methods can modify object state. They clarify intent, signaling to other developers which methods constitute the public interface and which are internal helpers.
The choice of access modifier affects maintainability and evolution of code. Public methods form contracts that must remain stable across versions. Private methods can be refactored freely without affecting external code. Protected methods enable specific sharing patterns between related classes.
Key Principles
Encapsulation represents the bundling of data and methods that operate on that data within a single unit, while restricting direct access to internal components. Access modifiers enforce encapsulation by controlling which parts of an object are visible externally versus hidden internally.
Information hiding separates interface from implementation. Public methods expose what an object can do, while private methods hide how it accomplishes those tasks. This separation allows internal implementation to change without affecting code that uses the class.
Least privilege principle suggests granting the minimum access necessary for functionality. Default to private for new methods, making them public only when external access is required. This approach reduces the API surface area and provides more flexibility for future changes.
Access control operates at the method level in Ruby. Unlike some languages that control access to instance variables directly, Ruby always keeps instance variables private. Access to instance variables occurs only through methods, which can have different access levels.
class User
def initialize(name, email)
@name = name
@email = email
end
# Public interface
def display_name
format_name(@name)
end
def change_email(new_email)
validate_email(new_email)
@email = new_email
end
private
# Implementation details
def format_name(name)
name.strip.split.map(&:capitalize).join(' ')
end
def validate_email(email)
raise ArgumentError unless email.include?('@')
end
end
Method visibility scope in Ruby is determined by where the access modifier appears in the class definition. Methods defined after an access modifier keyword inherit that visibility until another modifier changes it. This differs from Java's per-method modifiers.
Implicit receivers interact with access modifiers in Ruby. Private methods cannot be called with an explicit receiver, even self. This restriction prevents calling private methods on other instances of the same class, which differs from languages like Java where private methods are accessible to all instances of a class.
class Temperature
def initialize(celsius)
@celsius = celsius
end
def compare_to(other)
# celsius > other.celsius # This fails - private method
celsius > other.send(:celsius) # Must use send for private access
end
private
def celsius
@celsius
end
end
Protected methods provide a middle ground, callable with an explicit receiver only from within the class or its subclasses. This enables instances of a class to access each other's protected methods, supporting certain design patterns like comparison operations.
The distinction between class methods and instance methods affects access control. Class methods have their own visibility, separate from instance methods. A private class method is callable only within the class's singleton class context.
Ruby Implementation
Ruby implements access modifiers through three keywords: public, private, and protected. These keywords function as method calls rather than declarations, changing the visibility of methods defined afterward.
The public modifier makes methods callable from anywhere. Methods are public by default, so the public keyword is primarily used to restore public visibility after defining private or protected methods.
class Calculator
def add(a, b)
a + b
end
private
def validate_input(value)
raise ArgumentError unless value.is_a?(Numeric)
end
public
def multiply(a, b)
validate_input(a)
validate_input(b)
a * b
end
end
The private modifier restricts method access to within the defining class, without an explicit receiver. Private methods cannot be called with . notation, even with self as the receiver (with one exception: setter methods can be called with self. to disambiguate from local variable assignment).
class Person
def initialize(name, age)
@name = name
@age = age
end
def have_birthday
self.age = age + 1 # Setter requires self. prefix
end
def older_than?(other)
age > other.age # Error: private method
end
private
attr_accessor :age
attr_reader :name
end
The protected modifier allows methods to be called with an explicit receiver, but only from within the class or its subclasses. This enables instances to access each other's protected methods, which is particularly useful for comparison operations.
class Employee
attr_reader :name
def initialize(name, salary)
@name = name
@salary = salary
end
def earns_more_than?(other)
salary > other.salary # Works with protected
end
protected
attr_reader :salary
end
alice = Employee.new("Alice", 75000)
bob = Employee.new("Bob", 65000)
alice.earns_more_than?(bob) # => true
# bob.salary # => NoMethodError: protected method
Symbol-based access control allows specifying visibility for specific methods. Pass method names as symbols to access modifier methods to set their visibility without affecting subsequent method definitions.
class Document
def title
@title
end
def content
@content
end
def metadata
@metadata
end
private :metadata
public :title, :content
end
Module inclusion and access modifiers interact in specific ways. When a module is included in a class, the module's methods adopt the access level of the inclusion point. Methods can be made private or protected at inclusion time.
module Auditable
def log_action(action)
puts "Action: #{action}"
end
end
class SecureDocument
include Auditable
private :log_action
def update_content(new_content)
log_action("content updated")
@content = new_content
end
end
Class methods have their own visibility controls, defined within the singleton class. Use class << self to define class methods with specific visibility.
class Database
class << self
def connect(url)
connection = establish_connection(url)
authenticate(connection)
connection
end
private
def establish_connection(url)
# Connection logic
end
def authenticate(connection)
# Authentication logic
end
end
end
Method visibility changes can occur after initial definition using private, protected, or public with method name symbols. This allows adjusting visibility of methods defined elsewhere, including in parent classes or mixed-in modules.
class RestrictedArray < Array
private :clear, :delete, :delete_at
end
The Module#private_class_method and Module#public_class_method methods specifically control class method visibility. These methods operate on singleton class methods.
class Factory
def self.create_instance
new
end
def self.validate_config
# Validation logic
end
private_class_method :validate_config
end
Practical Examples
Data encapsulation protects object invariants by hiding internal state and exposing controlled access points. Consider a temperature class that maintains consistency between different units.
class Temperature
def initialize(celsius)
@celsius = celsius.to_f
end
def celsius
@celsius
end
def fahrenheit
celsius_to_fahrenheit(@celsius)
end
def kelvin
celsius_to_kelvin(@celsius)
end
def celsius=(value)
@celsius = value.to_f
end
def fahrenheit=(value)
@celsius = fahrenheit_to_celsius(value)
end
private
def celsius_to_fahrenheit(c)
(c * 9.0 / 5.0) + 32
end
def fahrenheit_to_celsius(f)
(f - 32) * 5.0 / 9.0
end
def celsius_to_kelvin(c)
c + 273.15
end
end
temp = Temperature.new(25)
temp.celsius # => 25.0
temp.fahrenheit # => 77.0
temp.fahrenheit = 100
temp.celsius # => 37.77...
Builder pattern implementation uses private constructors (or initialization details) to enforce object creation through a builder interface. Ruby's private_class_method makes the constructor private.
class HttpRequest
attr_reader :method, :url, :headers, :body
def initialize(method, url, headers, body)
@method = method
@url = url
@headers = headers
@body = body
end
private_class_method :new
def self.build
builder = Builder.new
yield builder
new(builder.method, builder.url, builder.headers, builder.body)
end
class Builder
attr_accessor :method, :url, :headers, :body
def initialize
@headers = {}
@method = :get
end
def header(key, value)
@headers[key] = value
self
end
end
end
request = HttpRequest.build do |b|
b.method = :post
b.url = "https://api.example.com/users"
b.header("Content-Type", "application/json")
b.body = '{"name": "Alice"}'
end
State validation ensures objects maintain valid states by making mutation methods private and exposing only validated operations. This prevents external code from putting objects into inconsistent states.
class ShoppingCart
def initialize
@items = []
@checked_out = false
end
def add_item(item, quantity = 1)
raise "Cannot modify checked out cart" if @checked_out
validate_item(item)
validate_quantity(quantity)
@items << { item: item, quantity: quantity }
end
def total
@items.sum { |entry| entry[:item].price * entry[:quantity] }
end
def checkout
validate_cart_not_empty
@checked_out = true
process_payment
end
private
def validate_item(item)
raise ArgumentError, "Invalid item" unless item.respond_to?(:price)
end
def validate_quantity(quantity)
raise ArgumentError, "Quantity must be positive" unless quantity > 0
end
def validate_cart_not_empty
raise "Cannot checkout empty cart" if @items.empty?
end
def process_payment
# Payment processing logic
end
end
Protected comparison operations use protected methods to enable instances to compare themselves while preventing external access to sensitive data.
class CreditCard
attr_reader :last_four_digits
def initialize(number, cvv, expiry)
@number = number
@cvv = cvv
@expiry = expiry
@last_four_digits = number[-4..]
end
def same_card?(other)
return false unless other.is_a?(CreditCard)
full_number == other.full_number
end
def secure_display
"**** **** **** #{@last_four_digits}"
end
protected
def full_number
@number
end
end
card1 = CreditCard.new("1234567890123456", "123", "12/25")
card2 = CreditCard.new("1234567890123456", "123", "12/25")
card1.same_card?(card2) # => true
# card1.full_number # => NoMethodError: protected method
Common Patterns
Template method pattern uses public methods to define algorithm structure while allowing subclasses to override private helper methods that implement specific steps.
class Report
def generate
prepare_data
format_header
format_body
format_footer
finalize
end
private
def prepare_data
# Default implementation
end
def format_header
raise NotImplementedError
end
def format_body
raise NotImplementedError
end
def format_footer
# Default implementation
end
def finalize
# Default implementation
end
end
class PdfReport < Report
private
def format_header
# PDF-specific header formatting
end
def format_body
# PDF-specific body formatting
end
end
Facade pattern exposes a simplified public interface while hiding complex subsystem interactions in private methods. This reduces coupling and simplifies client code.
class OrderProcessor
def process_order(order)
validate_order(order)
charge_payment(order)
update_inventory(order)
send_confirmation(order)
schedule_shipping(order)
end
private
def validate_order(order)
# Complex validation logic
end
def charge_payment(order)
# Payment processing
end
def update_inventory(order)
# Inventory management
end
def send_confirmation(order)
# Email sending
end
def schedule_shipping(order)
# Shipping coordination
end
end
Strategy pattern with private strategies keeps strategy implementations private while exposing a single public method that delegates to the appropriate strategy.
class TextFormatter
def initialize(format)
@format = format
end
def format(text)
case @format
when :html
format_html(text)
when :markdown
format_markdown(text)
when :plain
format_plain(text)
end
end
private
def format_html(text)
"<p>#{text}</p>"
end
def format_markdown(text)
text
end
def format_plain(text)
text.gsub(/<[^>]+>/, '')
end
end
Lazy initialization pattern uses private methods to handle deferred object creation, exposing only the public accessor method.
class DatabaseConnection
def connection
@connection ||= establish_connection
end
def query(sql)
connection.execute(sql)
end
private
def establish_connection
# Expensive connection setup
# Returns connection object
end
def connection_params
{
host: ENV['DB_HOST'],
port: ENV['DB_PORT'],
database: ENV['DB_NAME']
}
end
end
Protected factory methods allow subclasses to create related objects while preventing external instantiation.
class Vehicle
def self.manufacture(type)
case type
when :car
Car.build
when :truck
Truck.build
end
end
protected
def self.build
new
end
end
class Car < Vehicle
end
class Truck < Vehicle
end
# Vehicle.build # => NoMethodError: protected method
vehicle = Vehicle.manufacture(:car) # Works through public interface
Common Pitfalls
Private methods with explicit self fail in Ruby because private methods cannot have an explicit receiver. The exception is setter methods, which require self. to disambiguate from local variable assignment.
class Account
def update_balance(amount)
# self.balance = balance + amount # Works for setter
self.calculate_fee(amount) # NoMethodError
end
private
attr_writer :balance
def calculate_fee(amount)
amount * 0.01
end
end
Protected method assumptions from other languages do not transfer to Ruby. Protected methods in Ruby are callable with an explicit receiver only from within the class and subclasses, not from external code. This differs from Java where protected methods are accessible within the same package.
class Base
protected
def protected_method
"protected"
end
end
obj = Base.new
# obj.protected_method # => NoMethodError: protected method
Inheritance visibility changes can surprise developers. Subclasses cannot override a method with more restrictive access. A public method in the parent cannot become private in the child through normal override syntax.
class Parent
def public_method
"public"
end
end
class Child < Parent
private
# This makes NEW methods private, doesn't change public_method
def another_method
"private"
end
end
Child.new.public_method # Still accessible
Module inclusion timing matters for access modifiers. Methods included from modules adopt the visibility at the point of inclusion. Applying access modifiers before including a module has no effect on the included methods.
class Document
private
include Auditable # Auditable methods are private
end
class Report
include Auditable
private :log_action # Correct way to make specific module methods private
end
Reflection bypasses access control entirely. Methods like send, instance_eval, and instance_variable_get circumvent access modifiers, accessing private and protected members. This is sometimes necessary but breaks encapsulation.
class Secret
private
def secret_method
"secret"
end
end
obj = Secret.new
# obj.secret_method # NoMethodError
obj.send(:secret_method) # => "secret" - bypasses access control
Class variable visibility does not exist in Ruby. Class variables (@@variable) are always shared across the class hierarchy and accessible from any class method or instance method, regardless of access modifiers on methods that reference them.
class Counter
@@count = 0
def increment
@@count += 1
end
private
def count
@@count # Method is private, but class variable isn't
end
end
Testing private methods creates tension between encapsulation and testability. Directly testing private methods breaks encapsulation, but some complex private methods warrant direct testing. Options include: testing through public interface, extracting to separate class, using send in tests, or reconsidering visibility.
class ComplexCalculator
def calculate(input)
preprocess(input)
# More logic
end
private
def preprocess(input)
# Complex preprocessing that might warrant testing
end
end
# In tests:
calculator = ComplexCalculator.new
result = calculator.send(:preprocess, test_input) # Testing private method
Subclass access to private methods does not work as in some other languages. Private methods in a parent class are not callable from child classes, even without an explicit receiver. Protected methods should be used when subclass access is intended.
class Parent
private
def helper
"helper"
end
end
class Child < Parent
def call_helper
helper # NoMethodError in some Ruby versions, or works in others
end
end
Reference
Access Modifier Comparison
| Modifier | Callable With Receiver | Callable From Subclass | Callable From External Code | Primary Use Case |
|---|---|---|---|---|
| public | Yes | Yes | Yes | External interface methods |
| private | No (except self. for setters) | No | No | Internal implementation details |
| protected | Yes (within class/subclass) | Yes | No | Shared behavior between instances |
Visibility Control Methods
| Method | Purpose | Example Usage |
|---|---|---|
| public | Sets methods to public visibility | public :method_name |
| private | Sets methods to private visibility | private :method_name |
| protected | Sets methods to protected visibility | protected :method_name |
| private_class_method | Makes class methods private | private_class_method :class_method |
| public_class_method | Makes class methods public | public_class_method :class_method |
| module_function | Creates module functions (private instance, public module) | module_function :method_name |
Method Definition Patterns
| Pattern | Syntax | Effect |
|---|---|---|
| Bulk visibility | private def method1; end; def method2; end | Multiple methods private |
| Selective visibility | private :method1, :method2 | Specific methods private |
| Inline visibility | private def method; end | Single method private (Ruby 2.1+) |
| Restore visibility | public; def method; end | Return to public visibility |
Access Control Context
| Context | Behavior | Notes |
|---|---|---|
| Same instance | Private methods accessible without receiver | Within same object |
| Different instance, same class | Private methods not accessible | Even within same class |
| Different instance with protected | Protected methods accessible | Between instances of same class |
| Subclass instance | Protected methods accessible | Inherited protected access |
| Module included methods | Adopt visibility at inclusion point | Can be modified after inclusion |
Common Access Patterns
| Pattern | Access Level | Reason |
|---|---|---|
| Accessor methods | Public | External read/write access |
| Validation methods | Private | Internal consistency checks |
| Comparison helpers | Protected | Instance-to-instance comparison |
| Factory methods | Public | Object creation interface |
| Initialization helpers | Private | Setup logic |
| Template method hooks | Private/Protected | Subclass customization points |
| State mutation | Private | Controlled state changes |
Reflection Methods for Access Control
| Method | Purpose | Breaks Encapsulation |
|---|---|---|
| send | Call any method regardless of visibility | Yes |
| public_send | Call only public methods | No |
| instance_eval | Execute code in object context | Yes |
| instance_variable_get | Read instance variables directly | Yes |
| instance_variable_set | Write instance variables directly | Yes |
| method | Get Method object for any method | Partial |
Visibility Checking Methods
| Method | Returns | Use Case |
|---|---|---|
| public_methods | Array of public method names | Introspection |
| private_methods | Array of private method names | Debugging |
| protected_methods | Array of protected method names | Debugging |
| respond_to? | Boolean indicating method existence | Duck typing |
| respond_to?(:method, true) | Boolean including private methods | Comprehensive checking |
Module Function Pattern
| Usage | Result | Access Pattern |
|---|---|---|
| module_function :method | Creates both instance and module method | Instance private, module public |
| Include module | Instance methods available | Follow class visibility rules |
| Extend module | Module methods become instance methods | Follow visibility rules |
Access Modifier Scope Rules
| Scope | Applies To | Duration |
|---|---|---|
| After modifier keyword | All subsequent methods | Until next modifier |
| Symbol arguments | Named methods only | Immediate, no duration |
| Inline modifier | Single method | Immediate |
| Class definition | Methods in that class | Class definition scope |
| Module definition | Methods in that module | Module definition scope |