Наследование и 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 показывает эту цепочку.