RubyMiddleTechnical

Что такое 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; end
  • attr_writer :name — создаёт метод def name=(val); @name = val; end
  • attr_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.

Sources

Related topics