CrackedRuby logo

CrackedRuby

attr_accessor, attr_reader, attr_writer

Overview

Ruby provides three metaprogramming methods for generating instance variable accessors: attr_reader, attr_writer, and attr_accessor. These methods create getter methods, setter methods, or both respectively for specified instance variables. Ruby defines these methods in the Module class, making them available to all classes and modules.

When attr_reader :name executes during class definition, Ruby generates a method equivalent to def name; @name; end. The attr_writer :name method generates def name=(value); @name = value; end. The attr_accessor :name method generates both getter and setter methods.

class Person
  attr_reader :birth_date
  attr_writer :password
  attr_accessor :name, :age
end

person = Person.new
person.name = "Alice"
person.age = 30
# person.birth_date = Date.today  # NoMethodError - no setter
# puts person.password            # NoMethodError - no getter
puts person.name  # => "Alice"

Ruby executes these attribute methods during class definition, not when instances are created. The methods become part of the class definition and remain available to all instances. Multiple attributes can be declared in a single call by passing multiple symbols.

class Book
  attr_accessor :title, :author, :pages
  
  def initialize(title, author, pages)
    @title = title
    @author = author  
    @pages = pages
  end
end

book = Book.new("1984", "Orwell", 328)
book.title = "Animal Farm"  # Uses generated setter
puts book.author            # Uses generated getter

The generated methods access instance variables with the same name as the attribute symbol, prefixed with @. If the instance variable does not exist when a getter method is called, Ruby returns nil without raising an exception.

Basic Usage

The most common pattern involves using attr_accessor for attributes that need both reading and writing access. This approach works for simple data storage where no validation or transformation is required.

class Vehicle
  attr_accessor :make, :model, :year
  
  def initialize(make, model, year)
    @make = make
    @model = model
    @year = year
  end
  
  def description
    "#{@year} #{@make} #{@model}"
  end
end

car = Vehicle.new("Toyota", "Camry", 2020)
car.year = 2021
puts car.description  # => "2021 Toyota Camry"

Use attr_reader for read-only attributes that should not be modified after initialization. This pattern protects important data from accidental modification while maintaining access for reading.

class BankAccount
  attr_reader :account_number, :creation_date
  attr_accessor :balance
  
  def initialize(account_number)
    @account_number = account_number
    @creation_date = Time.now
    @balance = 0
  end
  
  def deposit(amount)
    @balance += amount if amount > 0
  end
end

account = BankAccount.new("12345")
puts account.account_number  # => "12345"
account.deposit(100)
puts account.balance         # => 100
# account.account_number = "67890"  # NoMethodError

The attr_writer method creates write-only access, commonly used for sensitive data like passwords where reading back the value is discouraged or unnecessary.

class User
  attr_reader :username
  attr_writer :password
  attr_accessor :email
  
  def initialize(username)
    @username = username
  end
  
  def authenticate(password)
    @password == password
  end
end

user = User.new("alice")
user.password = "secret123"
user.email = "alice@example.com"
puts user.username  # => "alice"
# puts user.password  # NoMethodError - no getter method

Attribute methods accept multiple symbols, allowing declaration of several attributes with the same access pattern in a single line. This syntax keeps class definitions concise when many attributes share the same access requirements.

class Product
  attr_reader :id, :created_at, :updated_at
  attr_writer :internal_notes, :vendor_code
  attr_accessor :name, :price, :description, :category
  
  def initialize(id, name, price)
    @id = id
    @name = name
    @price = price
    @created_at = Time.now
    @updated_at = Time.now
  end
  
  def update_price(new_price)
    @price = new_price
    @updated_at = Time.now
  end
end

Advanced Usage

Attribute methods can be combined with custom getter and setter methods to provide validation, transformation, or computed values. When custom methods exist with the same name as an attribute, the custom method takes precedence.

class Temperature
  attr_reader :celsius
  
  def initialize(celsius)
    @celsius = celsius
  end
  
  # Custom getter that computes value
  def fahrenheit
    (@celsius * 9.0 / 5.0) + 32
  end
  
  # Custom setter with validation
  def celsius=(value)
    raise ArgumentError, "Temperature cannot be below absolute zero" if value < -273.15
    @celsius = value
  end
  
  # Custom setter with transformation
  def fahrenheit=(value)
    self.celsius = (value - 32) * 5.0 / 9.0
  end
end

temp = Temperature.new(25)
puts temp.fahrenheit     # => 77.0
temp.fahrenheit = 100
puts temp.celsius        # => 37.77777777777778

Module inheritance affects attribute method availability. When a module defines attribute methods, classes that include the module gain access to those attributes and their generated methods.

module Timestamped
  def self.included(base)
    base.attr_reader :created_at, :updated_at
  end
  
  def initialize(*args)
    super
    @created_at = Time.now
    @updated_at = Time.now
  end
  
  def touch
    @updated_at = Time.now
  end
end

class Article
  include Timestamped
  attr_accessor :title, :content
  
  def initialize(title, content)
    @title = title
    @content = content
    super()
  end
end

article = Article.new("Ruby Attributes", "Content here")
puts article.created_at.class  # => Time
article.touch
puts article.updated_at > article.created_at  # => true

Attribute methods respect method visibility declarations. Calling private, protected, or public before attribute declarations affects the visibility of the generated methods.

class SecureDocument
  attr_accessor :title, :author
  
  private
  
  attr_accessor :encryption_key, :security_level
  attr_reader :access_log
  
  public
  
  def initialize(title, author)
    @title = title
    @author = author
    @encryption_key = generate_key
    @security_level = :standard
    @access_log = []
  end
  
  def access_document
    @access_log << Time.now
    decrypt_content
  end
  
  private
  
  def decrypt_content
    # Uses private encryption_key accessor
    puts "Decrypting with key: #{encryption_key}"
  end
  
  def generate_key
    SecureRandom.hex(16)
  end
end

doc = SecureDocument.new("Secret Plan", "Agent Smith")
puts doc.title  # => "Secret Plan"
# puts doc.encryption_key  # NoMethodError - private method

Class-level attribute management can be achieved by defining attribute methods on the singleton class. This pattern creates class-level instance variables with accessor methods.

class Configuration
  class << self
    attr_accessor :database_url, :api_key, :debug_mode
    
    def reset
      @database_url = nil
      @api_key = nil
      @debug_mode = false
    end
  end
  
  # Initialize class-level defaults
  reset
end

Configuration.database_url = "postgres://localhost/myapp"
Configuration.api_key = "abc123"
Configuration.debug_mode = true

puts Configuration.database_url  # => "postgres://localhost/myapp"
Configuration.reset
puts Configuration.debug_mode    # => false

Common Pitfalls

The primary confusion involves choosing between attr_reader, attr_writer, and attr_accessor. Many developers default to attr_accessor when attr_reader would be more appropriate, inadvertently exposing setter methods for data that should remain immutable.

# Problematic - exposes unnecessary setter
class Order
  attr_accessor :id, :created_at, :total  # id and created_at shouldn't be writable
end

order = Order.new
order.id = 12345
order.created_at = Time.now
# Later code accidentally modifies these
order.id = 99999          # Breaks referential integrity
order.created_at = nil    # Corrupts audit trail

# Better approach
class Order
  attr_reader :id, :created_at
  attr_accessor :total
  
  def initialize
    @id = generate_id
    @created_at = Time.now
  end
end

Attribute methods do not automatically initialize instance variables. Accessing a getter method for an uninitialized instance variable returns nil, which can cause unexpected behavior when the application expects a different default value.

class Counter
  attr_accessor :count
  
  def increment
    @count += 1  # TypeError if @count is nil
  end
end

counter = Counter.new
# counter.increment  # TypeError: nil can't be coerced into Integer

# Solution: Initialize in constructor or use custom getter
class Counter
  attr_writer :count
  
  def initialize
    @count = 0
  end
  
  def count
    @count ||= 0  # Lazy initialization alternative
  end
  
  def increment
    self.count += 1
  end
end

Inheritance hierarchies can create confusion when subclasses redefine attribute methods. The most recently defined method wins, which can mask inherited behavior unexpectedly.

class Animal
  attr_accessor :name
  
  def name=(value)
    @name = value.to_s.downcase
  end
end

class Dog < Animal
  attr_accessor :name  # Overwrites custom setter from Animal
end

animal = Animal.new
animal.name = "FLUFFY"
puts animal.name  # => "fluffy" (downcased)

dog = Dog.new  
dog.name = "ROVER"
puts dog.name     # => "ROVER" (not downcased - uses generated setter)

# Solution: Don't redeclare attributes in subclasses, or explicitly call super
class Dog < Animal
  def name=(value)
    super(value)  # Preserves parent behavior
  end
end

Thread safety issues arise when multiple threads access generated setter methods simultaneously. The assignment operation is not atomic, creating race conditions during concurrent access.

class SharedCounter
  attr_accessor :value
  
  def initialize
    @value = 0
  end
  
  def increment
    # Race condition: read-modify-write is not atomic
    self.value = value + 1
  end
end

# Multiple threads calling increment can lose updates
counter = SharedCounter.new

threads = 10.times.map do
  Thread.new do
    1000.times { counter.increment }
  end
end

threads.each(&:join)
puts counter.value  # Often less than 10,000 due to race conditions

# Solution: Use synchronization
class ThreadSafeCounter
  attr_reader :value
  
  def initialize
    @value = 0
    @mutex = Mutex.new
  end
  
  def increment
    @mutex.synchronize { @value += 1 }
  end
  
  def value=(new_value)
    @mutex.synchronize { @value = new_value }
  end
end

Serialization libraries may interact unexpectedly with attribute methods. Some serializers rely on method introspection and may include or exclude attributes based on the presence of getter/setter methods rather than instance variable content.

require 'json'

class Person
  attr_reader :name
  attr_writer :password
  
  def initialize(name, password)
    @name = name
    @password = password
    @internal_id = SecureRandom.uuid
  end
  
  def to_json(*args)
    # Only includes attributes with getters
    { name: @name }.to_json(*args)
    # @password and @internal_id are excluded
  end
end

person = Person.new("Alice", "secret")
puts person.to_json  # => {"name":"Alice"}
# Password not serialized (good), but internal_id also missing (maybe bad)

Reference

Core Methods

Method Parameters Returns Description
attr_reader(*symbols) Variable number of symbols Array of symbols Creates getter methods for each symbol
attr_writer(*symbols) Variable number of symbols Array of symbols Creates setter methods for each symbol
attr_accessor(*symbols) Variable number of symbols Array of symbols Creates both getter and setter methods

Generated Method Signatures

Attribute Declaration Generated Method Signature Behavior
attr_reader :name name def name; @name; end Returns instance variable value
attr_writer :name name= def name=(val); @name = val; end Sets instance variable value
attr_accessor :name name and name= Both getter and setter Combines reader and writer

Method Visibility Rules

Visibility Modifier Placement Effect
private Before attr declaration Generated methods are private
protected Before attr declaration Generated methods are protected
public Before attr declaration Generated methods are public (default)

Common Patterns

Pattern Use Case Example
Read-only data IDs, timestamps, computed values attr_reader :id, :created_at
Write-only data Passwords, sensitive input attr_writer :password, :secret
General attributes User data, configuration attr_accessor :name, :email
Mixed access Combination of above attr_reader :id; attr_accessor :name

Inheritance Behavior

Scenario Result
Parent has attr_reader :name Child inherits getter method
Child redefines attr_accessor :name Child's methods override parent's
Module defines attributes Including class gains attribute methods
Class extends module with attributes Singleton methods added to class

Thread Safety Considerations

Operation Thread Safety Mitigation
Reading attribute value Safe None required
Writing attribute value Unsafe Use Mutex or other synchronization
Read-modify-write operations Unsafe Synchronize entire operation
Initialization in initialize Safe Single-threaded during object creation

Common Error Patterns

Error Type Cause Solution
NoMethodError Calling setter on attr_reader Use attr_accessor or attr_writer
TypeError on arithmetic Uninitialized numeric attribute Initialize in constructor or use custom getter
Unexpected nil values Accessing unset attributes Provide default values or validation
Method masking Subclass redefines attribute Avoid redefinition or call super
Race conditions Concurrent access to setters Use thread synchronization primitives

Performance Characteristics

Aspect Performance Notes
Method generation Class definition time No runtime overhead
Getter method calls Direct instance variable access Equivalent to manual getter
Setter method calls Direct instance variable assignment Equivalent to manual setter
Memory overhead One method object per attribute Minimal per-class overhead

Debugging Methods

Method Returns Purpose
instance_variables Array of symbols Lists all instance variables
methods Array of symbols Lists all available methods
private_methods Array of symbols Lists private methods
respond_to?(:method) Boolean Checks method availability
method(:name).source_location Array or nil Shows method definition location