Наследование в программировании
ООП. Часть 5. Наследование и ещё немного полиморфизма
Вы всё время пользуетесь результатами наследования, даже если не знаете этого. Рассказываем, как меньше дублировать код и что общего у всех классов.
Оглавление:
Вот мы и подобрались к последнему столпу объектно-ориентированного программирования — наследованию. С его помощью можно создавать классы с общим функционалом, не копируя каждый раз одни и те же поля и методы.
Евгений Кучерявый
Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Как наследовать класс
Для начала создадим класс, от которого будем наследовать. Обычно его называют базовым или родительским:
Этот класс (Vehicle) представляет собой транспортное средство, но пока у него есть только слишком общие свойства (название, координаты и скорость) и поведение (перемещение). Нам может понадобиться реализовать класс, который тоже относится к транспортным средствам, но более конкретным. Например, это будет автомобиль (Car).
Если мы хотим, чтобы класс Car наследовал поля и методы класса Vehicle, то при его объявлении после названия нужно поставить двоеточие и имя родительского класса:
Теперь объекты класса Car обладают всеми полями и методами класса Vehicle:
Внимание! Наследовать можно только от одного класса.
Добавление новых полей и методов
Чтобы добавить в дочерний класс новое поле или метод, нужно просто объявить их:
Теперь объекты этого класса могут использовать как метод Move (), так и метод Beep (). То же самое касается и полей.
Наследование конструкторов
Допустим, у родительского класса есть конструктор, который принимает один аргумент:
Все дочерние классы должны вызывать его в своих конструкторах, передавая аргумент того же типа. Для этого используется ключевое слово base:
В скобках после base указывается аргумент, который нужно передать в родительский класс. При этом повторно описывать логику присваивания name не нужно.
Если вы не хотите ничего вызывать, то просто создайте в наследуемом классе пустой конструктор.
Переопределение методов
Часто бывает нужно, чтобы какой-то метод в дочернем классе работал немного иначе, чем в родительском. Например, в методе Move () для класса Car можно прописать условие, которое будет проверять, не кончилось ли топливо. Точно так же может появиться необходимость переопределить свойство.
Методы и свойства, которые можно переопределить, называются виртуальными. В родительском классе для них указывается модификатор virtual:
А в дочернем для переопределения используется модификатор override:
Таким образом можно определить разную логику для разных классов. Это тоже можно считать полиморфизмом.
Наследование от класса Object
Несмотря на то что наследовать можно только от одного класса, существует также и класс Object, который является родительским для всех остальных. У него есть четыре метода:
- Equals () — проверяет, равен ли текущий объект тому, что был передан в аргументе.
- ToString () — преобразует объект в строку.
- GetHashCode () — получает числовой хеш объекта. Этот метод редко используется, потому что может возвращать одинаковый хеш для разных объектов.
- GetType () — получает тип объекта.
Хеш — результат преобразования данных, который используется в криптографии.
Любой из них также может быть переопределён или перегружен. Например, метод Equals () можно использовать, чтобы он проверял, равны ли поля объектов:
В данном случае это именно перегрузка, потому что ни один из вариантов метода Equals () не принимал объект класса Car. Отсюда следует, что переопределить можно только метод с такими же принимаемыми аргументами.
Особенности наследования
Есть несколько особенностей, которые нужно знать при работе с наследованием:
- Наследовать можно только от класса, уровень доступа которого выше дочернего или равен ему. То есть публичный класс не может наследоваться от приватного.
- Дочерний класс не может обращаться к приватным полям и методам родительского. Поэтому нужно либо определять логику приватных компонентов в базовом классе, либо создавать публичные свойства и методы, которые будут своего рода посредниками.
- У дочернего класса может быть только один родительский, но у родительского может быть несколько дочерних.
- Нельзя наследовать от класса с модификатором static.
- Можно наследовать от класса, который наследует от другого класса. Но с этим лучше не злоупотреблять, потому что можно быстро запутаться в их взаимосвязях.
Чтобы лучше это усвоить, стоит попробовать поработать с каждой особенностью на практике и немного поэкспериментировать.
Домашнее задание
Создайте несколько классов персонажей: например, воин, лучник и маг.
Каждый из них должен быть родительским для нескольких других классов допустим, воин будет базовым классом для рыцаря и берсеркера.
У всех персонажей должен быть метод Attack (), при вызове которого у разных персонажей будут выводиться различные сообщения. Например, если атаковать будет маг, то мы должны увидеть сообщение, что он запустил огненный шар.
Заключение
С помощью наследования можно создавать множество полезных классов с общим поведением и свойствами, при этом не дублируя код. Однако это ещё не всё, что можно использовать, — в следующей статье вы узнаете про интерфейсы и абстрактные классы.
А чтобы на практике узнать, как используется ООП со всеми его особенностями, записывайтесь на курс «C#-разработчик с 0 до PRO». Вы попробуете разрабатывать на C# сайты и десктопные приложения, выжимая максимум из объектно-ориентированного программирования.
Наследование (программирование)
- Найти и оформить в виде сносок ссылки на авторитетные источники, подтверждающие написанное.
Насле́дование — механизм объектно-ориентированного программирования (наряду с инкапсуляцией, полиморфизмом и абстракцией), позволяющий описать новый класс на основе уже существующего (родительского), при этом свойства и функциональность родительского класса заимствуются новым классом.
Другими словами, класс-наследник реализует спецификацию уже существующего класса (базовый класс). Это позволяет обращаться с объектами класса-наследника точно так же, как с объектами базового класса.
Содержание
Типы наследования
Простое наследование
Класс, от которого произошло наследование, называется базовым или родительским (англ. base class ). Классы, которые произошли от базового, называются потомками, наследниками или производными классами (англ. derived class ).
В некоторых языках используются абстрактные классы. Абстрактный класс — это класс, содержащий хотя бы один абстрактный метод, он описан в программе, имеет поля, методы и не может использоваться для непосредственного создания объекта. То есть от абстрактного класса можно только наследовать. Объекты создаются только на основе производных классов, наследованных от абстрактного. Например, абстрактным классом может быть базовый класс «сотрудник вуза», от которого наследуются классы «аспирант», «профессор» и т. д. Так как производные классы имеют общие поля и функции (например, поле «год рождения»), то эти члены класса могут быть описаны в базовом классе. В программе создаются объекты на основе классов «аспирант», «профессор», но нет смысла создавать объект на основе класса «сотрудник вуза».
Множественное наследование
При множественном наследовании у класса может быть более одного предка. В этом случае класс наследует методы всех предков. Достоинства такого подхода в большей гибкости. Множественное наследование реализовано в C++. Из других языков, предоставляющих эту возможность, можно отметить Python и Эйфель. Множественное наследование поддерживается в языке UML.
Множественное наследование — потенциальный источник ошибок, которые могут возникнуть из-за наличия одинаковых имен методов в предках. В языках, которые позиционируются как наследники C++ (Java, C# и др.), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость все-таки возникла, то, для разрешения конфликтов использования наследованных методов с одинаковыми именами, возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.
Попытка решения проблемы наличия одинаковых имен методов в предках была предпринята в языке Эйфель, в котором при описании нового класса необходимо явно указывать импортируемые члены каждого из наследуемых классов и их именование в дочернем классе.
Большинство современных объектно-ориентированных языков программирования (C#, Java, Delphi и др.) поддерживают возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.
C ++ — Наследование
Одним из наиболее важных понятий объектно-ориентированного программирования является наследование. Наследование позволяет нам определить класс в терминах другого класса, что упрощает создание и обслуживание приложения. Это также дает возможность повторно использовать функциональность кода и быстрое время выполнения.
При создании класса вместо написания совершенно новых членов данных и функций-членов программист может обозначить, что новый класс должен наследовать членов существующего класса. Этот существующий класс называется базовым классом, а новый класс называется производным .
Идея наследования реализует это отношения. Например, млекопитающее животное IS-A, млекопитающее собаки IS-A, следовательно, животное IS-A собаки и так далее.
Базовые и производные классы
Класс может быть получен из более чем одного класса, что означает, что он может наследовать данные и функции из нескольких базовых классов. Чтобы определить производный класс, мы используем список производных классов, чтобы указать базовый класс (es). Список дериваций классов называет один или несколько базовых классов и имеет форму —
Если спецификатор доступа является одним из общедоступных, защищенных или закрытых , а базовым классом является имя ранее определенного класса. Если спецификатор доступа не используется, он по умолчанию является закрытым.
Рассмотрим базовый класс Shape и его производный класс Rectangle следующим образом:
Когда приведенный выше код компилируется и выполняется, он производит следующий результат:
Контроль доступа и наследование
Производный класс может получить доступ ко всем не-частным членам своего базового класса. Таким образом, члены базового класса, которые не должны быть доступны для функций-членов производных классов, должны быть объявлены частными в базовом классе.
Мы можем суммировать различные типы доступа в зависимости от того, кто может получить к ним доступ следующим образом:
Access
public
protected
private
Производный класс наследует все методы базового класса со следующими исключениями:
- Конструкторы, деструкторы и конструкторы копирования базового класса.
- Перегруженные операторы базового класса.
- Друг-функции базового класса.
Тип наследования
При выводе класса из базового класса базовый класс может наследоваться через общедоступное, защищенное или частное наследование. Тип наследования определяется спецификатором доступа, как описано выше.
Едва ли мы использовать защищенный или частное наследование, но общественное наследование обычно используется. При использовании другого типа наследования применяются следующие правила:
- Public Inheritance — При выводе класса из общедоступногобазового класса публичные члены базового класса становятся общедоступными членами производного класса, а защищенныечлены базового класса становятся защищенными членами производного класса. Частные члены базового класса никогда не доступны непосредственно из производного класса, но могут быть доступны через вызовы для публичных и защищенных членов базового класса.
- Protected legacy. При получении из защищенногобазового класса общедоступные и защищенные члены базового класса становятся защищенными членами производного класса.
- Private Inheritance — При получении из частного базового класса общедоступные и защищенные члены базового класса становятся частными членами производного класса.
Многократное наследование
Класс C ++ может наследовать членов из более чем одного класса, и вот расширенный синтаксис —
Если доступ является одним из общедоступных, защищенных или закрытых и будет предоставлен для каждого базового класса, они будут разделены запятой, как показано выше. Попробуем следующий пример —
Когда приведенный выше код компилируется и выполняется, он производит следующий результат:
Чем прототипное наследование отличается от классического?
Этот пост посвящен простому диалогу, который можно часто услышать к примеру на интервью на должность веб-разработчика:
ИНТЕРВЬЮЕР: Какой тип наследования в JavaScript?
КАНДИДАТ: Очевидно, что в JavaScript прототипное наследование.
ИНТЕРВЬЮЕР: Хорошо, а чем прототипное наследование отличается от классического наследования ООП?
А вот дальше кандидату следует уточнить что имеется ввиду. Если подразумевается что от него ожидается, что он начнет рассказывать о том как устроено прототипное наследование в JavaScript то это один момент, а если от него ожидает рассказ, о том чем парадигма прототипного наследования отличается от классического то это совсем другое.
В первом случае все достаточно просто, и как правило все кто хоть немного знает JavaScript готовы быстро ответить на этот вопрос (ну или произнести нужные ключевые слова типа атрибут __proto__, метод prototype и так далее).
А вот во втором случае все сложнее. Можно проработать 100 лет веб программистом, но толком не обратить внимание на этот вопрос. На практике с ним редко когда столкнешься. Но тем не менее, найти ответ на него было очень интересно.
Итак я попробую в этом посте освятить разницу между парадигмами прототипного и классического наследования. Прошу не судить меня слишком строго, если вы будете не согласны с моим мнением прошу в комментарии.
Как прототипное наследование, так и классическое наследование являются парадигмами объектно-ориентированного программирования (т. е. они имеют дело с объектами). Объекты – это просто абстракции, которые состоят из свойств сущности из реального мира (т. е. они представляют в программе сущности из реального мира в виде слов ). И это называется абстракция.
То есть абстракция – это представление вещей реального мира в компьютерных программах.
Теоретически абстракция определяется как «общая концепция, сформированная путем извлечения общих черт из конкретных примеров». Именно ради этого объяснения мы будем использовать вышеупомянутое определение.
Некоторые объекты имеют много общего. Например, ботинок имеет гораздо много общего с туфлей, и нечего с деревом. И поэтому ботинок и туфлю можно обобщить и назвать обувь. Так или иначе, не задумываясь об это, мы постоянно создаем эти ментальные организации.
Туфля и Ботинок – это Обувь. Следовательно, обувь – это обобщение как туфли, так и ботинка.
В приведенном выше примере обувь, туфля и ботинок – все это абстракции. Однако обувь является более общей абстракцией ботинка и туфли.
Обобщение так же может быть абстракция другой более конкретной абстракции.
Так как в объектно-ориентированном программировании мы постоянно должны создавать абстракции то для этого были придуманы два инструмента объекты и классы, а так же наследование. Через наследования мы создаем обобщения. Обувь – это обобщение ботинка. Следовательно ботинок должен наследовать от обуви.
Цель объектно-ориентированного программирования – максимально точно имитировать эти категории реального мира. Давайте возьмем наш пример обуви и пойдем дальше.
Парадигма классического наследования
В классическом наследовании объекты являются абстракциями «вещей» реального мира, но мы можем ссылаться на объекты только через классы. Классы – в данном случае это обобщение объекта. Другими словами, получается что классы – это абстракция объекта реального мира. При обобщение мы наследуем один класс от другого. И при классическом наследование процесс наследования должен создавать уровень абстракции. При каждом наследование, каждый дочерний класс должен повышать уровень абстракции, тем самым повышая уровень обобщения. Вот пример классического наследования:
Как вы можете видеть в классических объектно-ориентированных языках программирования классы являются обобщениями и при каждом наследования у них должен снижаться уровень абстракции.
Объекты в классических объектно-ориентированных языках программирования могут быть созданы только путем создания экземпляров классов.
Следовательно, по мере увеличения уровня абстракции сущности становятся более общими, а по мере снижения уровня абстракции сущности становятся более конкретными. В этом смысле уровень абстракции аналогичен шкале, варьирующейся от более специфических сущностей до более общих сущностей.
Задача программиста при использование парадигмы классического наследования создать иерархию сущностей от максимальной общей до максимально конкретной.
Парадигма прототипного наследования
В отличие от классического наследования, прототипное наследование не имеет дело с увеличивающимися уровнями абстракции. Объект – это либо абстракция реальной вещи, как и раньше, либо прямая копия другого Объекта (другими словами, Прототипа (Prototype)). Объекты могут быть созданы из ничего, или они могут быть созданы из других объектов.
Если взять наш прежний пример то вряд ли нам удастся избежать иерархии абстракций. Поэтому он будет выглядеть примерно так:
Но в нем есть главное отличие. shoe, boot и hikingBoot это все независимые объекты. Просто одни объекты созданы от других. Это важно! А при классическом наследование обобщения являются абстракциями абстракций… от абстракций … вплоть до самого последнего потомка.
Было бы правильнее в данном случае привести пример из независимых объектов, но для объяснения разницы думаю этого будет достаточно.
Уровень абстракции здесь не обязан быть глубже одного уровня (хотя при желание может и быть).
При использование парадигмы прототипного наследования программист имеет дело только с объектами и при этом у него есть возможность создавать сущности в одном уровне абстракции.
Вы можете комбинировать обе формы наследования для достижения очень гибкой системы повторного использования кода. Что собственно почти всегда и происходит в реальном коде JavaScript. То есть в реальный проектах обычно подсознательно реализуется классическое наследование, через иерархию объектов, хотя это делать не обязательно. Так как реализовать классическое наследование с помощью прототипов очень легко. Обратное кстати утверждение будет неверным.
Прототипное наследование позволяет реализовать большинство важных функций, которые вы найдете в классических языках ООП. В JavaScript замыкания и фабричные функции позволяют реализовать приватное состояние, а функциональное наследование можно легко комбинировать с прототипами, что также позволяет использовать миксины.
Некоторые преимущества прототипного наследования:
Слабая связь. Экземпляр никогда не нуждается в прямой ссылке на родительский класс или прототип. Можно сохранить ссылку на прототип объекта, но это не рекомендуется, потому что это будет способствовать тесной связи в иерархии объектов – одна из самых больших ошибок классического наследования.
Плоские иерархии. С прототипным наследованием легко поддерживать плоские иерархии наследования – используя конкатенацию (выборочное использование свойств одного объекта для создания другого ) и делегирование (клонирование одного объекта в другой), вы можете иметь один уровень делегирования объекта и один экземпляр без ссылок на родительские классы.
Тривиальное множественное наследование. Наследование от нескольких предков так же просто, как объединение свойств из нескольких прототипов с использованием конкатенации для формирования нового объекта или нового делегата для нового объекта.
Гибкая архитектура. Поскольку вы можете выборочно наследоваться, вам не нужно беспокоиться о проблеме «неправильного дизайна». Новый класс может наследовать любую комбинацию свойств от любой комбинации исходных объектов. Из-за простоты выравнивания иерархии, изменение в одном месте не обязательно вызывает рябь в длинной цепочке объектов-потомков.
Нет больше горилл с банами и джунглями. Селективное наследование устраняет эту проблему как и проблему ромба.
Заключение
В заключение отвечая на вопрос чем отличается классическое наследование от прототипного, можно сказать, что при следование парадигмы классического наследования нам необходимо создавать иерархию классов от общему к конкретному создавая тем самым при каждом наследование дополнительный уровнь абстракции. При следование парадигмы прототипного наследования мы не обязаны создавать создавать иерархию от общего к частному, мы можем это делать а может и не делать. Это оставляет нам свободу выбора (независимо от того понимаем мы это или нет), что и является на мой взгляд главным отличием этих двух парадигм.
Если у вас есть свое мнение на этот вопрос, добро пожаловать в комментарии.
Понятие наследования в программировании
Давайте рассмотрим такое понятие, как наследование в программировании.
Зачем это нужно и как это применять на практике?
Возможно, вы не раз видели такую картинку (см. видео), что есть общий (родительский) класс животные, который имеет дочерние элементы в виде кошек, собак др. объектов, которые являются подмножеством класса животные.
Давайте будем разбираться, как это может быть применимо к программированию.
Это теоретическая часть и мы не привязываемся ни к какому языку программирования.
Главная проблема при разработке больших и сложных приложений в том, что в них очень много объектов. См. видео картинку. Могут быть наземные, воздушные элементы и.т.д.
Таких объектов, из которых состоит программа, может быть очень и очень много. В играх — это персонажи этих игр.
На сайтах тоже бывают объекты, например, пост, страница, товар и.т.д. Это все объекты.
Основная сложность в управлении этими объектами.
Предположим, нам нужна команда для объектов, которые обладают каким-то общим свойством.
Например, мы разрабатываем игру и нам нужно дать команду всем воздушным элементам команду приземлиться.
Мы можем дать эту команду:
Или можно дать эту команду абстрактному понятию «летательный аппарат».
В итоге, все летательные аппараты должны пойти на посадку.
Обратите внимание, что количество строк кода уменьшилось до одной строки. Разница в эффективности очевидна.
Как же такое стало возможным, что столько много строк кода, преобразовались в одну?
Это стало возможным из-за того, что мы объединили наши объекты по какому-то общему признаку в отдельную сущность и назвали эту сущность «летательный_аппарат». Это и есть то самое наследование.
Т.е. наследование — это процесс при проектировании нашего приложения при котором мы объединяем какие-то объекты, которые имеют схожие характеристики в единую структуру с главным родительским элементом.
Наследование — это один из способов, как можно организовать код программы. Можно обойтись и без наследования.
Вопрос в том, насколько удобно вам будет в дальнейшем вашу программу расширять, пользоваться кодом этого приложения и взаимодействовать с такой программой.
При проектировании программы, все объекты отображаются в виде схемы, каждый объект прорисовывается в виде прямоугольника (см. видео). Вверху название этого объекта, а внизу, свойства и методы, которые этот объект принимает.
Наследование в такой диаграмме показывается в виде стрелки и такая диаграмма называется UML-диаграмма.
Например, у нас есть сайт и на этом сайте родительским классом будет являться Контент, а дочерними классами могут являться посты и страницы.
И у страниц и у постов есть заголовок, содержимое, комментарии, но каждый из дочерних элементов имеет свои особенные свойства или методы. Например, страница имеет категорию, пост имеет рубрики и.т.д.
Можно не применять наследование для такой структуры, но мы получаем дублирование свойств и методов в каждом классе.
+ Нет повтора кода
+ Простота расширения приложения
Если мы разрабатываем приложение и у нас появился еще один вид контента, например товар. У товара может быть также заголовок, контент и.т.д.
Мы можем просто расширить родительский класс контент и у нас уже появляется новый элемент контента.
+ Простота управления сущностями
Например, если нам нужно выполнить поиск по содержимому всего контента на сайте, намного проще (без использования сложных запросов к базе данных) это будет сделать в варианте, где используется наследование.
Во многих сложных и больших программах, вы можете увидеть ситуацию, что какой-то объект расширяет другие объекты. Есть родительские объекты, а есть дочерние объекты. Каким бы языком программирования вы бы не пользовались, такое понятие имеет место быть и с этим можно очень часто сталкиваться.
Надеюсь стало понятнее, что такое наследование и зачем это нужно и теперь вы можете посмотреть конструкции по наследованию, которые есть в вашем языке программирования.
Главное понимать смысл и процесс программирования должен стать для вас намного более удобнее, проще и приятнее.
Чтобы оставить сообщение, зарегистрируйтесь/войдите на сайт через:
Или зарегистрируйтесь через социальные сети: