CrackedRuby logo

CrackedRuby

Access Control

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