Классы, объекты и состояние

Класс — это чертёж, объект — изделие по нему. Ruby объектно-ориентирован до мозга костей, и собственные классы — естественный способ моделировать предметную область.
Суть: класс описывается через class; метод initialize — конструктор, вызываемый при .new; переменные экземпляра с @ хранят состояние объекта; attr_accessor генерирует методы доступа к ним.

До сих пор мы пользовались готовыми классами (String, Array, Hash). Теперь создадим свои. Класс группирует данные (состояние) и поведение (методы) в одну сущность. Каждый объект класса хранит собственное состояние в переменных экземпляра — они начинаются с @ и невидимы снаружи напрямую.

class Dog
  def initialize(name, age)
    @name = name      # переменная экземпляра
    @age = age
  end

  def bark
    "#{@name} говорит: Гав!"
  end
end

rex = Dog.new("Рекс", 3)   # вызывает initialize
puts rex.bark              # => Рекс говорит: Гав!

Разбор: доступ к состоянию через attr_accessor

Переменные @name закрыты снаружи. Чтобы их читать и менять, нужны методы. Писать их вручную утомительно, поэтому Ruby даёт макросы: attr_reader (только чтение), attr_writer (только запись), attr_accessor (и то, и другое). Они генерируют методы за вас.

class Account
  attr_accessor :balance   # создаёт balance и balance=
  attr_reader :owner       # только чтение

  def initialize(owner)
    @owner = owner
    @balance = 0
  end
end

acc = Account.new("Аня")
acc.balance = 100      # работает методом balance=
puts acc.balance       # => 100
puts acc.owner         # => Аня

Как работает под капотом

Когда вы пишете Dog.new("Рекс", 3), Ruby создаёт пустой объект, затем вызывает на нём initialize с вашими аргументами и возвращает готовый объект. Состояние живёт внутри: у каждого объекта своя копия @name и @age. А attr_accessor :balance — это не «свойство», а сокращение, которое определяет два метода: balance (возвращает @balance) и balance= (присваивает).

   класс Dog (чертёж)
        |
   Dog.new("Рекс",3)
        |
   1. создать пустой объект
   2. вызвать initialize(name, age)
        |  @name="Рекс"  @age=3
        v
   объект rex  [ @name, @age ]   <- своё состояние
   объект bob  [ @name, @age ]   <- другое состояние
        |
   у каждого объекта СВОИ @-переменные

Та же идея «класс с конструктором и свойствами» на Python:

# Та же логика на Python ▶
class Dog:
    def __init__(self, name, age):
        self.name = name      # аналог @name
        self.age = age

    def bark(self):
        return f"{self.name} говорит: Гав!"

rex = Dog("Рекс", 3)
print(rex.bark())   # Рекс говорит: Гав!

Частые ошибки

  • Забыть @ у переменной экземпляра. Без @ это локальная переменная метода, она исчезнет после выхода и не сохранит состояние.
  • Открывать всё через attr_accessor. Не каждое поле должно быть изменяемым снаружи. Чаще достаточно attr_reader.
  • Логика в initialize. Конструктор должен только инициализировать состояние, а не выполнять тяжёлую работу.

Best practices

  • По умолчанию открывайте только чтение (attr_reader); запись добавляйте, лишь когда она действительно нужна.
  • Имена классов пишите в CamelCase: BankAccount, а не bank_account.
  • Держите состояние объекта согласованным: проверяйте входные данные в initialize, чтобы объект не родился в «битом» виде.

Глубже: методы класса и переменные класса

До сих пор мы говорили о методах экземпляра — тех, что вызываются на конкретном объекте. Но у класса есть и собственные методы и данные, общие для всех экземпляров. Метод класса объявляется через def self.method_name и вызывается прямо на классе: классический пример — фабричные методы вроде User.create или Time.now, которые создают и возвращают объекты. Такие методы удобны, когда действие логически принадлежит классу в целом, а не отдельному экземпляру. Рядом существуют переменные класса (@@count), общие для всех объектов, — например, счётчик созданных экземпляров. Однако к ним стоит относиться осторожно: они разделяются по всей иерархии наследования и легко приводят к неожиданному поведению, поэтому в современном коде их часто заменяют переменными экземпляра самого класса. Понимание разделения «уровень класса против уровня экземпляра» — важная веха: оно объясняет, почему Array.new вызывается на классе, а arr.push — на объекте, и готовит вас к пониманию того, как устроены константы, конфигурация и фабрики в больших Ruby-проектах и в Rails.

Итог. Класс — чертёж, объект — экземпляр со своим состоянием в @-переменных. initialize — конструктор, а attr_accessor и его собратья генерируют методы доступа, избавляя от рутины.

Проверьте себя
1. Что делает метод initialize в классе Ruby?
AУдаляет объект
BЭто конструктор: он вызывается при .new и задаёт начальное состояние объекта
CСоздаёт класс
DПечатает объект
2. Что генерирует объявление attr_accessor :balance?
AТолько переменную @balance
BМетоды чтения balance и записи balance=
CКласс Balance
DКонстанту BALANCE