Наследование и super

Наследование позволяет одному классу перенять поведение другого и дополнить или изменить его. Это способ переиспользовать код без копирования.
Суть: класс наследуется через class Child < Parent; наследник получает все методы родителя, может их переопределять, а super вызывает версию родителя.

Если у вас есть общий класс Animal, то Dog и Cat разумно сделать его наследниками: они — частные случаи животного. Наследник получает всё поведение родителя бесплатно и добавляет своё. Связь наследования в Ruby обозначается символом «меньше» <.

class Animal
  def initialize(name)
    @name = name
  end

  def describe
    "#{@name} — это животное"
  end
end

class Dog < Animal           # Dog наследует Animal
  def speak
    "Гав!"
  end
end

rex = Dog.new("Рекс")
puts rex.describe   # => Рекс — это животное (метод родителя)
puts rex.speak      # => Гав! (собственный метод)

Разбор: super и переопределение

Наследник может переопределить метод родителя, дав ему новое тело. А чтобы не выбрасывать поведение родителя целиком, а дополнить его, используют super — вызов одноимённого метода родителя. Без скобок super передаёт те же аргументы, со скобками — указанные явно.

class Cat < Animal
  def initialize(name, color)
    super(name)          # вызвать Animal#initialize
    @color = color
  end

  def describe
    super + ", #{@color} кот"   # дополнить ответ родителя
  end
end

murka = Cat.new("Мурка", "рыжая")
puts murka.describe   # => Мурка — это животное, рыжая кот

Как работает под капотом: цепочка предков

Когда вы вызываете метод, Ruby ищет его по «цепочке предков»: сначала в самом классе объекта, затем в его родителе, родителе родителя и так далее до BasicObject. Первый найденный метод и выполняется. super же продолжает поиск с того места цепочки, где мы сейчас находимся, — вверх. Увидеть цепочку можно методом .ancestors.

puts Cat.ancestors.inspect
# => [Cat, Animal, Object, Kernel, BasicObject]
   murka.describe
        |
        v
   [ Cat ] есть describe? --ДА--> выполняем
        |                          |
        |                       super --> ищем выше
        v                          v
   [ Animal ] есть describe? --ДА--> выполняем
        |
        v
   [ Object ] --> [ BasicObject ]  (конец цепочки)

Та же идея наследования и вызова родителя на Python:

# Та же логика на Python ▶
class Animal:
    def __init__(self, name):
        self.name = name
    def describe(self):
        return f"{self.name} — это животное"

class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name)      # вызов родителя
        self.color = color
    def describe(self):
        return super().describe() + f", {self.color} кот"

print(Cat("Мурка", "рыжая").describe())

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

  • Забыть super в initialize. Переопределив initialize и не вызвав super, вы пропустите инициализацию родителя — состояние будет неполным.
  • Глубокие иерархии. Пять уровней наследования — сигнал, что задачу лучше решать модулями (следующий урок), а не деревом классов.
  • Путать super и super(). super без скобок шлёт те же аргументы; super() с пустыми скобками — ни одного. Это разные вызовы.

Best practices

  • Наследуйте по принципу «является» (Dog является Animal), а не «имеет» — для «имеет» используйте композицию.
  • Не делайте иерархии глубже двух-трёх уровней; для разделяемого поведения берите модули.
  • Вызывайте super в переопределённом initialize, чтобы родитель корректно настроил своё состояние.

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

Наследование — мощный, но коварный инструмент, и зрелые разработчики относятся к нему с осторожностью. Проблема глубоких иерархий в том, что они создают жёсткую связанность: изменение в базовом классе откликается во всех потомках, иногда неожиданно. Знаменитый принцип «предпочитай композицию наследованию» предлагает альтернативу: вместо того чтобы класс был чем-то (наследование), пусть он имеет что-то (композиция) — хранит другой объект и делегирует ему часть работы. Например, вместо класса ElectricCar < Car < Vehicle часто чище сделать Car, который содержит объект-двигатель, и подменять двигатель. В Ruby для делегирования есть удобные инструменты вроде модуля Forwardable. Практическое правило: наследование оправдано, когда отношение «является» очевидно и стабильно (квадрат является фигурой), а в спорных случаях лучше композиция или модули-миксины. Не воспринимайте это как запрет на наследование — оно прекрасно работает для неглубоких, ясных иерархий. Просто держите в голове, что глубокое дерево классов почти всегда сигнализирует, что какую-то его часть стоило бы вынести в отдельный объект или модуль.

Итог. Наследование (class Child < Parent) передаёт наследнику поведение родителя; super вызывает родительскую версию метода. Поиск метода идёт по цепочке предков снизу вверх до BasicObject, и .ancestors показывает эту цепочку.

Проверьте себя
1. Что делает вызов super внутри переопределённого метода?
AСоздаёт новый объект
BВызывает одноимённый метод родительского класса
CЗавершает программу
DДелает метод приватным
2. Как Ruby ищет метод при его вызове на объекте?
AТолько в самом классе объекта
BПо цепочке предков: класс, родитель, родитель родителя — до BasicObject
CСлучайным образом
DТолько в модуле Kernel