Introduction
In Ruby, instance variables (@name, @age) are private by default — they can’t be accessed from outside the class. To expose them, you need getter and setter methods. Ruby provides attr_reader, attr_writer, and attr_accessor as shortcuts to generate these methods automatically.
The Problem They Solve
Without accessors, you’d write getter and setter methods manually:
class Person
def initialize(name, age)
@name = name
@age = age
end
# Getter
def name
@name
end
# Setter
def name=(value)
@name = value
end
# Getter
def age
@age
end
# Setter
def age=(value)
@age = value
end
end
That’s a lot of boilerplate. The accessor macros replace all of it.
attr_reader: Read-Only Access
attr_reader generates only a getter method:
class BankAccount
attr_reader :balance, :account_number
def initialize(account_number, initial_balance)
@account_number = account_number
@balance = initial_balance
end
def deposit(amount)
@balance += amount
end
end
account = BankAccount.new("ACC-001", 1000)
puts account.balance # => 1000
puts account.account_number # => "ACC-001"
account.balance = 2000 # => NoMethodError: undefined method 'balance='
Use attr_reader when you want to expose a value but prevent external modification.
attr_writer: Write-Only Access
attr_writer generates only a setter method:
class Config
attr_writer :debug_mode
attr_writer :log_level
def initialize
@debug_mode = false
@log_level = :info
end
def status
"debug=#{@debug_mode}, level=#{@log_level}"
end
end
config = Config.new
config.debug_mode = true
config.log_level = :debug
puts config.status # => "debug=true, level=debug"
config.debug_mode # => NoMethodError: undefined method 'debug_mode'
attr_writer alone is rarely used — usually you want to read the value too.
attr_accessor: Read-Write Access
attr_accessor generates both getter and setter:
class Person
attr_accessor :name, :email, :age
def initialize(name, email, age)
@name = name
@email = email
@age = age
end
def introduce
"Hi, I'm #{name}, #{age} years old. Email: #{email}"
end
end
person = Person.new("Alice", "[email protected]", 30)
puts person.name # => "Alice"
puts person.introduce # => "Hi, I'm Alice, 30 years old. Email: [email protected]"
person.name = "Alicia"
person.age = 31
puts person.introduce # => "Hi, I'm Alicia, 31 years old. Email: [email protected]"
What They Generate
These macros generate real Ruby methods. attr_accessor :name is equivalent to:
def name
@name
end
def name=(value)
@name = value
end
You can verify this:
class Dog
attr_accessor :name
end
puts Dog.instance_methods(false).sort.inspect
# => [:name, :name=]
Mixing Accessor Types
Use different accessors for different attributes based on your encapsulation needs:
class User
attr_accessor :name, :email # read-write: users can update these
attr_reader :id, :created_at # read-only: set once, never changed externally
attr_writer :password # write-only: can set but never read back
def initialize(id, name, email, password)
@id = id
@name = name
@email = email
@created_at = Time.now
self.password = password # uses the setter (could add hashing here)
end
def authenticate(plain_password)
BCrypt::Password.new(@password) == plain_password
end
end
user = User.new(1, "Alice", "[email protected]", "secret123")
puts user.id # => 1
puts user.name # => "Alice"
user.name = "Alicia" # OK
user.id = 99 # => NoMethodError
user.password # => NoMethodError (write-only)
Custom Accessors with Validation
When you need validation or transformation, define the method manually instead of using the macro:
class Product
attr_reader :name, :price
def name=(value)
raise ArgumentError, "Name can't be blank" if value.to_s.strip.empty?
@name = value.strip
end
def price=(value)
raise ArgumentError, "Price must be positive" unless value.to_f > 0
@price = value.to_f.round(2)
end
end
p = Product.new
p.name = " Widget "
p.price = "19.999"
puts p.name # => "Widget" (stripped)
puts p.price # => 20.0 (rounded)
p.name = "" # => ArgumentError: Name can't be blank
p.price = -5 # => ArgumentError: Price must be positive
Accessor with Callbacks
You can override the generated setter to add side effects:
class Observable
attr_reader :value
def initialize(value)
@value = value
@callbacks = []
end
def value=(new_value)
old_value = @value
@value = new_value
@callbacks.each { |cb| cb.call(old_value, new_value) }
end
def on_change(&block)
@callbacks << block
end
end
counter = Observable.new(0)
counter.on_change { |old, new| puts "Changed: #{old} → #{new}" }
counter.value = 5 # => Changed: 0 → 5
counter.value = 10 # => Changed: 5 → 10
Protected and Private Accessors
You can restrict accessor visibility:
class Employee
attr_accessor :name
def higher_paid_than?(other)
salary > other.salary # can call protected method on another Employee
end
protected
attr_reader :salary
private
attr_writer :salary
public
def initialize(name, salary)
@name = name
@salary = salary
end
end
alice = Employee.new("Alice", 80_000)
bob = Employee.new("Bob", 70_000)
puts alice.higher_paid_than?(bob) # => true
puts alice.salary # => NoMethodError: protected method
Struct as an Alternative
For simple data containers, Struct auto-generates accessors and more:
# Equivalent to a class with attr_accessor for each field
Point = Struct.new(:x, :y) do
def distance_to(other)
Math.sqrt((x - other.x)**2 + (y - other.y)**2)
end
end
p1 = Point.new(0, 0)
p2 = Point.new(3, 4)
puts p1.x # => 0
puts p1.distance_to(p2) # => 5.0
puts p2.to_h.inspect # => {:x=>3, :y=>4}
Quick Reference
| Macro | Generates | Access |
|---|---|---|
attr_reader :x |
def x; @x; end |
Read only |
attr_writer :x |
def x=(v); @x=v; end |
Write only |
attr_accessor :x |
Both getter and setter | Read + Write |
Comments