что такое множественное наследование
Наследование в C++: beginner, intermediate, advanced
В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.
Beginner
Что такое наследование?
Наследование является одним из основополагающих принципов ООП. В соответствии с ним, класс может использовать переменные и методы другого класса как свои собственные.
Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.
Наследование полезно, поскольку оно позволяет структурировать и повторно использовать код, что, в свою очередь, может значительно ускорить процесс разработки. Несмотря на это, наследование следует использовать с осторожностью, поскольку большинство изменений в суперклассе затронут все подклассы, что может привести к непредвиденным последствиям.
Важное примечание: приватные переменные и методы не могут быть унаследованы.
Типы наследования
В C ++ есть несколько типов наследования:
Конструкторы и деструкторы
В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.
Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.
Множественное наследование
Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.
Проблематика множественного наследования
Множественное наследование требует тщательного проектирования, так как может привести к непредвиденным последствиям. Большинство таких последствий вызваны неоднозначностью в наследовании. В данном примере Laptop наследует метод turn_on() от обоих родителей и неясно какой метод должен быть вызван.
Несмотря на то, что приватные данные не наследуются, разрешить неоднозначное наследование изменением уровня доступа к данным на приватный невозможно. При компиляции, сначала происходит поиск метода или переменной, а уже после — проверка уровня доступа к ним.
Intermediate
Проблема ромба
Ромбовидная проблема — прежде всего проблема дизайна, и она должна быть предусмотрена на этапе проектирования. На этапе разработки ее можно разрешить следующим образом:
Проблема ромба: Конструкторы и деструкторы
Поскольку в С++ при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов, возникает и другая проблема: конструктор базового класса Device будет вызван дважды.
Виртуальное наследование
Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.
Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).
Абстрактный класс
В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.
Интерфейс
С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).
Advanced
Несмотря на то, что наследование — фундаментальный принцип ООП, его стоит использовать с осторожностью. Важно думать о том, что любой код который будет использоваться скорее всего будет изменен и может быть использован неочевидным для разработчика путем.
Наследование от реализованного или частично реализованного класса
Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.
В противовес этому стоит заметить что наследование от частично реализованных классов имеет неоспоримое преимущество. Библиотеки и фреймворки зачастую работают следующим образом: они предоставляют пользователю абстрактный класс с несколькими виртуальными и множеством реализованных методов. Таким образом, наибольшее количество работы уже проделано — сложная логика уже написана, а пользователю остается только кастомизировать готовое решение под свои нужды.
Интерфейс
Наследование от интерфейса (чистого абстрактного класса) преподносит наследование как возможность структурирования кода и защиту пользователя. Так как интерфейс описывает какую работу будет выполнять класс-реализация, но не описывает как именно, любой пользователь интерфейса огражден от изменений в классе который реализует этот интерфейс.
Интерфейс: Пример использования
Прежде всего стоит заметить, что пример тесно связан с понятием полиморфизма, но будет рассмотрен в контексте наследования от чистого абстрактного класса.
Приложение выполняющее абстрактную бизнес логику должно настраиваться из отдельного конфигурационного файла. На раннем этапе разработки, форматирование данного конфигурационного файла до конца сформировано не было. Вынесение парсинга файла за интерфейс предоставляет несколько преимуществ.
Отсутствие однозначности касательно форматирования конфигурационного файла не тормозит процесс разработки основной программы. Два разработчика могут работать параллельно — один над бизнес логикой, а другой над парсером. Поскольку они взаимодействуют через этот интерфейс, каждый из них может работать независимо. Данный подход облегчает покрытие кода юнит тестами, так как необходимые тесты могут быть написаны с использованием мока (mock) для этого интерфейса.
Также, при изменении формата конфигурационного файла, бизнес логика приложения не затрагивается. Единственное чего требует полный переход от одного форматирования к другому — написания новой реализации уже существующего абстрактного класса (класса-парсера). В дальнейшем, возврат к изначальному формату файла требует минимальной работы — подмены одного уже существующего парсера другим.
Заключение
Наследование предоставляет множество преимуществ, но должно быть тщательно спроектировано во избежание проблем, возможность для которых оно открывает. В контексте наследования, С++ предоставляет широкий спектр инструментов который открывает массу возможностей для программиста.
Простое введение в C++. Часть 4. Множественное наследование
Множественное наследование — это одна из ключевых особенностей языка C++. Рассмотрим, когда оно может потребоваться и как его использовать.
Назначение множественного наследования
Предположим, что нам нужно нарисовать на форме логотип, который состоит из квадрата и круга. Сначала посмотрим, как это сделать на Си. Предположим, что у нас уже есть функции рисования квадрата и круга.
Далее везде, где нужно рисование логотипа, вызываем функцию Logo.
Теперь нам нужно то же самое сделать в C++. Предположим, что у нас уже есть классы рисования квадрата и круга. Как нарисовать логотип?
Можно, конечно, сделать так:
А потом вызывать везде эту пару строк, но это процедурный подход и он в C++ не приветствуется. В тех случаях, когда нам нужно получить методы разных классов, как раз и используется множественное наследование.
Использование множественного наследования
Для поддержки множественного наследования нужно при определении класса перечислить через запятую базовые классы. В примере для простоты вместо рисования фигур методы Draw просто выводят их названия.
Отличия множественного наследования
В случае единичного наследования производный класс получает доступ к публичным и защищенным элементам базового класса.
В случае множественного наследования производный класс получает доступ к публичным и защищенным элементам всех базовых классов.
Кроме того, в момент создания экземпляра производного класса выполняться все конструкторы базовых классов, а при удалении объекта выполнятся все деструкторы базовых классов.
Ошибка неоднозначности
Если в базовых классах элементы называются одинаково, то в производном классе появляется ошибка неоднозначности. В примере выше, если написать просто
то компилятор выдаст ошибку, так непонятно метод какого класса нужно использовать. Ведь метод Draw есть в обоих классах.
Алмаз смерти
Теперь рассмотрим ситуацию, когда один производный класс имеет два базовых класса, при этом каждый из которых является производным одного и того же суперкласса. При этом в производных классах есть свойства с одинаковым именем.
Подобная ситуация получила название «алмаз смерти», так как на диаграмме классов такая ситуация выглядит как алмаз.
В рассмотренном примере добавим суперкласс Shape.
Проблемы множественного наследования в крупных проектах
В конце концов если классы пишет один человек, то он сам все-таки представляет, что делать в случае неоднозначности вызова. Но если это используется в группе, то ситуация резко усложняется.
Лучше всего эту картину иллюстрирует карточный домик.
Представьте себе, что каждая карта — это класс, который разрабатывает отдельный программист. Нижние карты — это базовые классы, а все остальные — производные классы. И теперь представьте, что вам достался самый верхний этаж.
Вы написали класс, запустили программу и получили «алмаз смерти». К какому программисту вы будете обращаться?
Запрет наследования
Посмотрим на проблему и с другой стороны. Представьте, что вы год назад написали класс, который находится в середине иерархии классов. Отладили его, провели тесты и с ним все в порядке. Сейчас вы пишете другой класс, при этом у вас, как обычно, что-то не получается и сроки поджимают.
В этот момент к вам подходит коллега и говорит:
— Слушай, я унаследовал твой класс и класс Угрюмова. И получил алмаз смерти. Иди и решай с Угрюмовым проблему неоднозначности.
Как вы понимаете ответить этому программисту хочется только одно:
— А может ты не будешь наследовать мой класс?!
Для подобных случаев в стандарт C++ 11 добавлен спецификатор final, который запрещает наследовать данный класс. Для этого нужно написать ключевое слово final после имени класса.
Урок №161. Множественное наследование
До сих пор мы рассматривали только одиночные наследования, когда дочерний класс имеет только одного родителя. Однако C++ предоставляет возможность множественного наследования.
Множественное наследование
Множественное наследование позволяет одному дочернему классу иметь несколько родителей. Предположим, что мы хотим написать программу для отслеживания работы учителей. Учитель — это Human. Тем не менее, он также является Сотрудником (Employee).
Множественное наследование может быть использовано для создания класса Teacher, который будет наследовать свойства как Human, так и Employee. Для использования множественного наследования нужно просто указать через запятую тип наследования и второй родительский класс:
Проблемы с множественным наследованием
Хотя множественное наследование кажется простым расширением одиночного наследования, оно может привести к множеству проблем, которые могут заметно увеличить сложность программ и сделать кошмаром дальнейшую поддержку кода. Рассмотрим некоторые из подобных ситуаций.
Во-первых, может возникнуть неоднозначность, когда несколько родительских классов имеют метод с одним и тем же именем, например:
При компиляции c54G.getID() компилятор смотрит, есть ли у WirelessAdapter метод getID(). Этого метода у него нет, поэтому компилятор двигается по цепочке наследования вверх и смотрит, есть ли этот метод в каком-либо из родительских классов. И здесь возникает проблема — getID() есть как у USBDevice, так и у NetworkDevice. Следовательно, вызов этого метода приведет к неоднозначности и мы получим ошибку, так как компилятор не будет знать какую версию getID() ему вызывать.
Тем не менее, есть способ обойти эту проблему. Мы можем явно указать, какую версию getID() следует вызывать:
Хотя это решение довольно простое, но всё может стать намного сложнее, если наш класс будет иметь от 4 родительских классов, которые, в свою очередь, будут иметь свои родительские классы. Возможность возникновения конфликтов имен увеличивается экспоненциально с каждым добавленным родительским классом, и в каждом из таких случаев нужно будет явно указывать версии методов, которые следует вызывать, дабы избежать возможности возникновения конфликтов имен.
Во-вторых, более серьезной проблемой является «алмаз смерти» (или «алмаз обреченности»). Это ситуация, когда один класс имеет 2 родительских класса, каждый из которых, в свою очередь, наследует свойства одного и того же родительского класса. Иллюстративно мы получаем форму алмаза.
Например, рассмотрим следующие классы:
Сканеры и принтеры — это устройства, которые получают питание от розетки, поэтому они наследуют свойства PoweredDevice. Однако ксерокс (Copier) включает в себя функции как сканеров, так и принтеров.
В этом контексте возникает много проблем, включая неоднозначность при вызове методов и копирование данных PoweredDevice в класс Copier дважды. Хотя большинство из этих проблем можно решить с помощью явного указания, поддержка и обслуживание такого кода может привести к непредсказуемым временным затратам. Мы поговорим детально о способах решения проблемы «алмаза смерти» на соответствующем уроке.
Стоит ли использовать множественное наследование?
Большинство задач, решаемых с помощью множественного наследования, можно решить и с использованием одиночного наследования. Многие объектно-ориентированные языки программирования (например, Smalltalk, PHP) даже не поддерживают множественное наследование. Многие, относительно современные языки, такие как Java и C#, ограничивают классы одиночным наследованием обычных классов, но допускают множественное наследование интерфейсных классов. Суть идеи, запрещающей множественное наследование в этих языках, заключается в том, что это излишняя сложность, которая порождает больше проблем, чем удобств.
Многие опытные программисты считают, что множественное наследование в языке C++ следует избегать любой ценой из-за потенциальных проблем, которые могут возникнуть. Однако все же остается вероятность, когда множественное наследование будет лучшим решением, нежели придумывание двухуровневых «костылей».
Стоит отметить, что вы сами уже использовали классы, написанные с использованием множественного наследования, даже не подозревая об этом: такие объекты, как std::cin и std::cout библиотеки iostream, реализованы с использованием множественного наследования!
Правило: Используйте множественное наследование только в крайних случаях, когда задачу нельзя решить одиночным наследованием, либо другим альтернативным способом (без изобретения «велосипеда»).
Множественное наследование в Java. Композиция в сравнении с Наследованием
Множественное наследование в Java
Ромбовидная проблема
Множественное наследование Интерфейсов
Композиция против Наследования
Предположим, что у нас есть суперкласс и класс, расширяющий его:
Обратите внимание, что метод test() уже существует в подклассе, но тип возвращаемого значения отличается. Теперь класс ClassD не будет компилироваться и если вы будете использовать какой-либо IDE, то она вам предложит изменить тип возвращаемого значения в суперклассе или подклассе.
Теперь представьте себе ситуацию, когда мы имеем многоуровневую иерархию наследования классов и не имеем доступа к суперклассу. У нас не будет никакого выбора, кроме как изменить нашу сигнатуру метода подкласса или его имя, чтобы удалить ошибку компиляции. Также мы должны будем изменить метод подкласса во всех местах, где он вызывается. Таким образом, наследование делает наш код хрупким.
Вышеупомянутая проблема никогда не произойдет с композицией и это делает ее более привлекательной по отношению к наследованию.
Другая проблема с наследованием состоит в том, что мы предоставляем все методы суперкласса клиенту и если наш суперкласс должным образом не разработан и есть пробелы в системе безопасности, то даже при том, что мы наилучшим образом выполняем реализацию нашего класса, на нас влияет плохая реализация суперкласса. Композиция помогает нам в обеспечении управляемого доступа к методам суперкласса, тогда как наследование не обеспечивает управления методами суперкласса. Это также одно из основных преимуществ композиции от наследования.
Результат программы представленной выше:
Эта гибкость при вызове метода не доступна при наследовании, что добавляет еще один плюс в пользу выбора композиции.
Поблочное тестирование легче делать при композиции, потому что мы знаем, что все методы мы используем от суперкласса и можем копировать их для теста. Тогда как в наследовании мы зависим в большей степени от суперкласса и не знаем всех методов суперкласса, которые будут использоваться. Таким образом, мы должны тестировать все методы суперкласса, что является дополнительной работой из-за наследования.
В идеале мы должны использовать наследование только когда отношение подкласса к суперклассу определяется как «является». Во всех остальных случаях рекомендуется использовать композицию.
Множественное наследование
Класс А обобщенно наследует элементы всех трех основных классов.
Для доступа к членам порожденного класса, унаследованного от нескольких базовых классов, используются те же правила, что и при порождении из одного базового класса. Проблемы могут возникнуть в следующих случаях:
В этих случаях необходимо использовать оператор разрешения контекста для уточнения элемента, к которому осуществляется доступ, именем базового класса.
Так как объект порожденного класса состоит из нескольких частей, унаследованных от базовых классов, то конструктор порожденного класса должен обеспечивать инициализацию всех наследуемых частей. В списке инициализации в заголовке конструктора порожденного класса должны быть перечислены конструкторы всех базовых классов. Порядок выполнения конструкторов при порождении из нескольких классов следующий:
Деструкторы вызываются в порядке обратном вызову конструкторов.
Z() < cout "Z destroyed" endl; cin.get(); >;
>;
class A : public X, public Y, public Z
<
int key;
public :
A( int i = 0) : X(i + 1), Y(i + 2), Z(i + 3)
<
key = X::key + Y::key + Z::key;
>
int getkey( void ) < return (key); >
>;
int main()
<
A object(4);
cout «object.key = » object.getkey();
cin.get();
return 0;
>
Виртуальные базовые классы
Если двойное вхождение объектов класса X в объект класса А не является допустимым, существует два выхода для разрешения такой ситуации:
Базовый класс определяется как виртуальный заданием ключевого слова virtual в списке порождения перед именем базового класса или указанием типа наследования
class A : public Y, public Z <
int key;
public :
A( int i = 0) : Y(i + 2), Z(i + 3)
<
key = Y::key + Z::key;
>
int getkey( void ) < return (key); >
>;
int main() <
A object(4);
cout «object.key = » object.getkey();
cin.get();
return 0;
>
Конструкторы и деструкторы при использовании виртуальных базовых классов выполняются в следующем порядке:
При порождении с использованием виртуальных базовых классов сохраняются те же правила видимости, что и при порождении с не виртуальными классами.