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 |