Наследование – одна из ключевых концепций объектно-ориентированного программирования. В большинстве языков наследование разрешено от единственного класса, но, например в С++ вы можете создать класс, который является наследником сразу нескольких базовых классов. Все это предоставляет программисту мощный инструмент для повторного использования кода. Но, как это часто бывает, вместе с большой силой приходит и большая ответственность. Неправильное использование наследования гарантировано приведет вас к усложнению архитектуры и неожиданным ошибкам в работе программы.
И хотя довольно часто может казаться, что два класса делают одно и тоже, это не означает, что один из них может быть наследником другого. И «человек», и «утка» могут крякать, но это ведь не означает, что они родственники? Что же делать, когда ну уж совсем не хочется дублировать один и тот же код в разных частях программы?
В качестве примера плохого наследования рассмотрим такой вариант:
class Vehicle
attr_reader :power
def do_whroom # делает Вруум!
# процедура подготовка двигателя для вруум!
"#{power}hp Whroom!"
end
end
class ElectroVehicle < Vehicle
def do_whroom # переопределяем метод и делает Зуум!
# процедура подготовка двигателя для зуум!
"#{power}hp Zoom!"
end
end
class Car < Vehicle
def beep # делает Бип!
# готовимся к бип!
"Beep!"
end
end
class ElectroCar < ElectroVehicle
def beep # делает Бип!
# готовимся к бип!
"Beep!"
end
end
class Airplane < Vehicle
def fly
# летим!
end
end
class Ford < Car
def initialize
@power = 100
end
end
class BMW < Car
def initialize
@power = 200
end
end
class Tesla < ElectroCar
def initialize;
@power = 150
end
end
class Boing < Airplane
def initialize
@power = 1000
end
end
У нас есть класс Vehicle (средство передвижения на ДВС), которое умеет издавать звук («Whroom!»). Так же у нас есть класс ElectroVehicle (средство передвижения на электродвигателе), который также умеет издавать звук («Зуум!»). Еще у нас есть наследники этих классов: Car (автомобиль), ElectroCar (электромобиль) и Airplane (самолет). Car и ElectroCar кроме прочего умеют издавать предупреждающий сигнал («Beep!»), а Airplane умеет летать. Также у нас есть несколько реализаций этих классов: автомобили Ford и BMW, электромобиль Tesla и самолет Boing. С первого взгляда может показаться, что все вышло довольно-таки неплохо, но это не так:
Мы имеем дублирование кода в классах Car и ElectroCar (метод beep). Это произошло из-за того, что мы не смогли разместить метод beep в общем предке Vehicle, потому что не все Vehicle умеют издавать предупреждающий сигнал.Классом ElectroVehicle мы переопределили, а не расширили метод do_whroom родителя. Тем самым мы нарушили принцип подстановки Барбары Лисков и в дальнейшем, когда в коде программы мы будем работать с экземпляром ElectroVehicle как с обычным Vehicle у нас будут проблемы. Например, наша программа будет ожидать появления строки «Whroom!» после вызова метода do_whroom, но сколько она не будет пытаться получить этот результат от Tesla — на выходе всегда будет «Zoom!»
Со временем, мы обнаружим, что электромобили — это не родственники автомобилей, но будет уже поздно. Чем больше мы будем писать кода в Vehicle, тем чаще нам будет необходимо переопределять его в ElectroVehicle.К счастью, есть такая вещь как композиция. Композиция – это техника программирования при которой новые классы создаются путем помещения обобщенных функциональных модулей в объект-контейнер, который впоследствии использует и управляет этими модулями. Если при наследовании говорят, что наследник «является» родителем (Пользователь IS_A Администратор), то при композиции говорят, что один объект «владеет» другим (Пользователь HAS_A РольАдминистратора).
На Ruby простая композиция для описанного выше примера может быть реализована следующим образом: