Что такое attr_accessor, attr_reader и attr_writer в Ruby?
attr_accessor создаёт и getter, и setter; attr_reader — только getter; attr_writer — только setter. Все три генерируют методы через Module#define_method во время интерпретации класса.
Что такое attr_accessor, attr_reader и attr_writer
В Ruby instance-переменные (@var) по умолчанию недоступны снаружи объекта. Чтобы открыть доступ, нужно вручную написать методы-getter и setter — или воспользоваться макросами Module#attr_*, которые генерируют эти методы автоматически.
attr_reader :name— создаёт методdef name; @name; endattr_writer :name— создаёт методdef name=(val); @name = val; endattr_accessor :name— создаёт оба метода сразу
Вызов этих макросов происходит на этапе загрузки класса (class body execution), а не в runtime каждого объекта. Под капотом Ruby вызывает define_method на модуле класса.
Пример
class User
attr_accessor :name, :email # getter + setter для обоих
attr_reader :id # только getter — id менять нельзя снаружи
attr_writer :password_hash # только setter — читать не должны
def initialize(id, name, email)
@id = id
@name = name
@email = email
end
end
user = User.new(1, 'Alice', 'alice@example.com')
puts user.name # => "Alice" (getter)
user.name = 'Bob' # setter
puts user.id # => 1
# user.id = 2 # NoMethodError: undefined method `id='
user.password_hash = 'bcrypt...' # setter работает
# user.password_hash # NoMethodError: undefined method `password_hash'
Ограничение доступа как проектное решение
Выбор между тремя макросами — это выражение инварианта объекта в коде:
- Если поле всегда устанавливается в
initializeи никогда не должно меняться снаружи — толькоattr_reader. - Если поле должно быть доступно для чтения внешним кодом и изменения —
attr_accessor. attr_writerбезattr_readerвстречается редко; типичный случай — injection зависимости, где класс принимает настройку, но не возвращает её наружу (например, пароль, токен).
Переопределение сгенерированных методов
class Product
attr_reader :price
def price=(value)
raise ArgumentError, 'Price must be positive' unless value.positive?
@price = value
end
end
Можно сначала объявить attr_writer, а затем переопределить setter с валидацией. Порядок объявления не важен — Ruby перезапишет метод последним определением.
protected и private для attr_*
class BankAccount
attr_reader :balance
private
attr_writer :balance # setter приватный — снаружи не вызвать
end
Размещение attr_writer после private делает setter недоступным вне класса. Это распространённый паттерн для защиты внутреннего состояния.
Подводные камни
- Избыточный attr_accessor: открывать setter для всех полей по умолчанию — нарушение инкапсуляции. Предпочитайте
attr_readerи явно разрешайте запись только там, где это нужно. - Символ vs строка:
attr_reader :nameработает,attr_reader "name"— тоже, но принято передавать символы. - Переопределение в subclass: если в подклассе объявить
attr_writer :field, а суперкласс уже имеетattr_reader :field, доступ будет работать — но убедитесь, что это ожидаемое поведение. - Конфликт с методами модулей: если включаемый модуль определяет метод с тем же именем, а класс после этого вызывает
attr_accessor, сгенерированный метод перекроет метод модуля. - Rspec и attr_reader: в тестах часто нужен доступ к приватному состоянию. Лучше тестировать через публичный интерфейс, а не обходить ограничения через
send. - Freeze и setter: вызов setter на замороженном объекте бросает
FrozenError. Если объект может быть заморожен (например, константы), убедитесь, что setters не вызываются послеfreeze. - Thread safety:
attr_accessorне является атомарной операцией в MRI при конкурентных записях. Для разделяемого состояния используйтеMutexилиConcurrent::AtomicReferenceиз concurrent-ruby.
Common mistakes
- Сводить attr accessors к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: объектная динамическая модель Ruby, где почти всё является объектом, а методы ищутся через ancestor chain.
- Не отделять validation, authorization, transaction boundary и business logic.
What the interviewer is testing
- Объясняет attr accessors через конкретную точку lifecycle в Ruby.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.