Перейти к основному содержимому

4.08. Наследование

Разработчику Аналитику Тестировщику
Архитектору Инженеру

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

Наследование – создание новых классов (типов объектов) на основе существующих, наследуя их свойства и методы. Позволяет создавать новый класс (подкласс) на основе существующего (родительского), перенимая его свойства и методы. Это уменьшает дублирование кода и упрощает его поддержку.

К примеру, у нас есть несколько классов, которые имеют общие атрибуты или методы - можно не дублировать их в каждом. а вынести в базовый класс. Подклассы будут наследовать эти общие элементы. И вместо того, чтобы писать одинаковый код для разных классов - используется наследование.

image-8.png


★ Подкласс сможет добавлять свои новые методы или переопределять существующие методы родительского класса.

★ Базовый класс (суперкласс) — это класс, который предоставляет свои свойства или методы для наследования. Он описывает общие характеристики и поведение, которые могут быть полезны для подклассов.


Пример:

класс Транспорт {
строка модель;
целое год;

метод запустить() {
вывод("Транспорт запущен.");
}
}

Здесь класс Транспорт является базовым классом. Он содержит общие атрибуты (модель, год) и метод (запустить()).


Производный класс (или подкласс) — это класс, который наследует свойства и методы базового класса. Подкласс может использовать унаследованные элементы «как есть», добавлять новые элементы или переопределять унаследованные методы.

Пример:

класс Автомобиль : Транспорт { // Наследование от Транспорт
целое количество_колёс;

метод ехать() {
вывод("Автомобиль едет.");
}

метод запустить() { // Переопределение метода
вывод("Автомобиль запущен!");
}
}

Здесь класс Автомобиль наследует модель, год и метод запустить() от класса Транспорт. Также он добавляет поле количество_колёс и метод ехать(), и кроме этого - переопределяет метод запустить().


Интерфейс — это набор методов, которые должны быть реализованы в классах, использующих этот интерфейс, и интерфейсы не содержат реализации методов. Они используются для определения «контракта», которому должны следовать классы.

Пример:

интерфейс Запускаемый {
метод запустить();
}

класс Автомобиль : Запускаемый {
метод запустить() {
вывод("Автомобиль запущен.");
}
}

Здесь класс Автомобиль реализует интерфейс Запускаемый и обязан реализовать метод запустить().

Абстрактный класс может содержать как реализованные методы, так и абстрактные методы (без реализации). От абстрактного класса нельзя создавать объекты напрямую. Он используется для создания базовых классов, которые будут наследоваться.

Пример:

абстрактный класс Транспорт {
метод запустить() {
вывод("Транспорт запущен.");
}

абстрактный метод ехать(); // Без реализации
}

класс Автомобиль : Транспорт {
метод ехать() {
вывод("Автомобиль едет.");
}
}

Здесь класс Автомобиль наследует абстрактный класс Транспорт и обязан реализовать метод ехать().


Множественное наследование — это возможность наследовать свойства и методы сразу нескольких классов. Это мощный механизм, но он может вызывать проблемы, такие как «конфликт имён» (если два родительских класса имеют методы с одинаковыми именами).

Пример:

класс Двигатель {
метод завести() {
вывод("Двигатель заведён.");
}
}

класс Колёса {
метод повернуть() {
вывод("Колёса повернуты.");
}
}

класс Автомобиль : Двигатель, Колёса { // Наследование от двух классов
метод ехать() {
завести();
повернуть();
вывод("Автомобиль едет.");
}
}

Здесь класс Автомобиль наследует методы от классов Двигатель и Колёса.

Важно: не все языки программирования поддерживают множественное наследование (например, Java не поддерживает, но позволяет использовать интерфейсы для достижения похожего эффекта).

Представим, что класс D наследуется от B и С. Оба класса B и C наследуются от общего предка - класса A. Если в классе A есть какой-то метод или поле, то возникает неоднозначность: когда мы обращаемся к этому методу или полю через объект D, какой именно экземпляр использовать — от B или от C? Компилятор не знает, вызывать ли B или C. У нас могут получиться два разных метода в классе D.

image-9.png

Это называется Diamond Problem (ромбическая проблема) - ситуация в ООП, когда класс наследуется от двух или более классов, которые имеют общий предок, что приводит к неясности - чей метод использовать в дочернем классе. Именно поэтому многие современные языки не поддерживают множественное наследование, а если два интерфейса содержат метод с одинаковой сигнатурой, то класс обязан явно переопределить его.

Python поддерживает множественное наследование и использует MRO (Method Resolution Order) — порядок разрешения методов, где Python сначала будет искать метод в B, потом в C и затем в A.

Иногда класс не должен быть наследуемым, чтобы предотвратить изменение его поведения. Это особенно важно для классов, реализующих важную логику. В некоторых языках есть ключевое слово, например final (Java), которое делает класс ненаследуемым - финальным:

final класс Утилиты {
метод выполнить() {
вывод("Выполняется...");
}
}

класс НовыйКласс : Утилиты { // Ошибка: нельзя наследовать final-класс
}

Здесь класс Утилиты защищён от наследования.