Мне всегда нравилась идея поддержки множественного наследования в языке. Чаще всего это преднамеренно, и предполагаемая «замена» - это интерфейсы. Интерфейсы просто не охватывают все то же основание, что и множественное наследование, и это ограничение может иногда приводить к дополнительному шаблонному коду.
Единственная основная причина, которую я когда-либо слышал для этого, - проблема алмазов с базовыми классами. Я просто не могу принять это. Для меня это звучит ужасно, например: «Ну, это возможно испортить, так что это автоматически плохая идея». Вы можете испортить что угодно на языке программирования, и я имею в виду все, что угодно. Я просто не могу принять это всерьез, по крайней мере, без более подробного объяснения.
Просто осознание этой проблемы - это 90% битвы. Кроме того, я думаю, что слышал что-то много лет назад о обходном пути общего назначения, включающем алгоритм «конверта» или что-то в этом роде (кто-нибудь звонит в колокол?).
Что касается проблемы с бриллиантами, единственная потенциально подлинная проблема, о которой я могу подумать, это если вы пытаетесь использовать стороннюю библиотеку и не видите, что два, казалось бы, не связанных класса в этой библиотеке имеют общий базовый класс, но в дополнение к документация, простая языковая функция, скажем, может потребовать, чтобы вы специально заявили о своем намерении создать бриллиант, прежде чем он фактически скомпилирует его для вас. С такой особенностью любое создание алмаза является либо преднамеренным, безрассудным, либо потому, что человек не знает об этой ловушке.
Так что все сказанное ... Есть ли какая-то реальная причина, по которой большинство людей ненавидят множественное наследование, или это просто кучка истерии, которая приносит больше вреда, чем пользы? Есть ли что-то, чего я здесь не вижу? Спасибо.
пример
Автомобиль расширяет WheeledVehicle, KIASpectra расширяет Car и Electronic, KIASpectra содержит радио. Почему KIASpectra не содержит Electronic?
Потому что это Электронный. Наследование против состава всегда должно быть отношением «есть» или «иметь».
Потому что это Электронный. Есть провода, платы, переключатели и т. Д. Все это вверх и вниз по этой вещи.
Потому что это Электронный. Если ваша батарея разряжается зимой, вы попадаете в такую же проблему, как если бы все ваши колеса внезапно пропали без вести.
Почему бы не использовать интерфейсы? Взять, к примеру, № 3. Я не хочу писать это снова и снова, и я действительно не хочу создавать какой-то причудливый прокси-класс помощника для этого:
private void runOrDont()
{
if (this.battery)
{
if (this.battery.working && this.switchedOn)
{
this.run();
return;
}
}
this.dontRun();
}
(Мы не понимаем, является ли эта реализация хорошей или плохой.) Вы можете себе представить, как могут быть некоторые из этих функций, связанных с Electronic, которые не связаны ни с чем в WheeledVehicle, и наоборот.
Я не был уверен, стоит ли останавливаться на этом примере или нет, так как там есть место для интерпретации. Вы могли бы также подумать о терминах, расширяющих Plane, и FlyingObject и Bird, расширяющих Animal и FlyingObject, или о более чистом примере.
Traits
- они действуют как интерфейсы с необязательной реализацией, но имеют некоторые ограничения, которые помогают предотвратить возникновение таких проблем, как проблема с бриллиантами.KiaSpectra
не являетсяElectronic
; у него есть электроника, и он может бытьElectronicCar
(который будет расширятьсяCar
...)Ответы:
Во многих случаях люди используют наследование, чтобы предоставить черту классу. Например, подумайте о Пегасе. При множественном наследовании у вас может возникнуть соблазн сказать, что Пегас расширяет Лошадь и Птицу, потому что вы классифицировали Птицу как животное с крыльями.
Однако у Птиц есть и другие черты, которых нет у Пегаси. Например, птицы откладывают яйца, у пегасов есть живорождение. Если наследование является вашим единственным средством передачи общих черт, то нет способа исключить черту яйцекладки из Пегаса.
Некоторые языки решили сделать черты явной конструкцией внутри языка. Другие мягко направляют вас в этом направлении, удаляя MI из языка. В любом случае, я не могу вспомнить ни одного случая, когда я подумал: «Чувак, мне действительно нужен МИ, чтобы сделать это правильно».
Также давайте обсудим, что такое НАСТОЯЩЕЕ наследование. Когда вы наследуете от класса, вы берете зависимость от этого класса, но также вы должны поддерживать контракты, которые поддерживает класс, как неявные, так и явные.
Возьмите классический пример квадрата, наследуемого от прямоугольника. Прямоугольник предоставляет свойство длины и ширины, а также методы getPerimeter и getArea. Квадрат переопределяет длину и ширину, поэтому, когда один из них установлен, другой устанавливается так, чтобы он соответствовал getPerimeter и getArea работал бы одинаково (2 * длина + 2 * ширина для периметра и длина * ширина для области).
Существует один тестовый случай, который ломается, если вы замените эту реализацию квадрата на прямоугольник.
Достаточно сложно разобраться с единой цепочкой наследования. Это становится еще хуже, когда вы добавляете еще один в микс.
Подводные камни, о которых я упоминал в Pegasus в MI, и отношения «Прямоугольник / Квадрат» - оба являются результатом неопытного дизайна для классов. По сути, избегание множественного наследования - это способ помочь начинающим разработчикам избежать стрельбы в ногу. Как и все принципы проектирования, дисциплина и основанная на них подготовка позволяют вовремя обнаружить, когда можно оторваться от них. См. Модель приобретения навыков Дрейфусом , на уровне эксперта ваши внутренние знания выходят за рамки принципов / принципов. Вы можете «чувствовать», когда правило не применяется.
И я согласен с тем, что я несколько обманул пример «реального мира», почему MI не одобряется.
Давайте посмотрим на структуру пользовательского интерфейса. В частности, давайте рассмотрим несколько виджетов, которые на первый взгляд могут выглядеть так, будто они представляют собой просто комбинацию двух других. Как ComboBox. ComboBox - это TextBox, поддерживающий DropDownList. Т.е. я могу ввести значение или выбрать из заранее определенного списка значений. Наивным подходом было бы наследовать ComboBox от TextBox и DropDownList.
Но ваше текстовое поле получает свою ценность от того, что набрал пользователь. В то время как DDL получает свое значение от того, что выбирает пользователь. Кто берет прецедент? DDL, возможно, был разработан для проверки и отклонения любого ввода, которого не было в его первоначальном списке значений. Мы отвергаем эту логику? Это означает, что мы должны предоставить внутреннюю логику для переопределения наследников. Или, что еще хуже, добавьте логику в базовый класс, который существует только для поддержки подкласса (нарушая принцип инверсии зависимости ).
Избежание ИМ поможет вам вообще обойти эту ловушку. И может привести к тому, что вы извлечете общие, повторно используемые черты ваших виджетов пользовательского интерфейса, чтобы их можно было применять по мере необходимости. Прекрасным примером этого является присоединенное свойство WPF, которое позволяет элементу каркаса в WPF предоставлять свойство, которое другой элемент каркаса может использовать без наследования от родительского элемента каркаса.
Например, Grid - это панель макета в WPF, к которой прикреплены свойства Column и Row, которые указывают, где дочерний элемент должен быть размещен в расположении сетки. Без вложенных свойств, если я хочу расположить кнопку в сетке, кнопка должна быть производной от сетки, чтобы иметь доступ к свойствам столбца и строки.
Разработчики развили эту концепцию и использовали присоединенные свойства как способ компоновки поведения (например, вот мой пост о создании сортируемого GridView с использованием вложенных свойств, написанных до того, как WPF включил DataGrid). Подход был признан в качестве шаблона проектирования XAML под названием « Присоединенное поведение» .
Надеюсь, это дало немного больше понимания того, почему множественное наследование обычно не одобряется.
источник
Разрешение множественного наследования делает правила о перегрузках функций и виртуальной диспетчеризации, безусловно, более сложными, а также реализацию языка вокруг макетов объектов. Это немного влияет на разработчиков / разработчиков языка и поднимает и без того высокую планку, чтобы сделать язык стабильным и приемлемым.
Другой распространенный аргумент, который я видел (и приводил время от времени), состоит в том, что имея два + базовых класса, ваш объект почти всегда нарушает принцип единой ответственности. Либо два + базовых класса являются хорошими автономными классами со своей собственной ответственностью (вызывающей нарушение), либо они являются частичными / абстрактными типами, которые работают друг с другом, создавая единую связную ответственность.
В этом другом случае у вас есть 3 сценария:
Лично я думаю, что множественное наследование имеет плохой рэп, и что хорошо сделанная система композиции стилей черт была бы действительно мощной / полезной ... но есть много способов, которыми она может быть реализована плохо, и по многим причинам это не очень хорошая идея в таком языке, как C ++.
[править] в отношении вашего примера, это абсурд. А у Киа есть электроника. У него есть двигатель. Точно так же у электроники есть источник питания, который просто оказывается автомобильным аккумулятором. Наследование, не говоря уже о множественном наследовании, там не место.
источник
Единственная причина, по которой он запрещен, заключается в том, что людям легко выстрелить себе в ногу.
В таком обсуждении обычно следуют аргументы относительно того, важнее ли гибкость наличия инструментов, чем безопасность, чтобы не отстреливаться. Нет однозначно правильного ответа на этот аргумент, потому что, как и большинство других вещей в программировании, ответ зависит от контекста.
Если ваши разработчики знакомы с MI, и MI имеет смысл в контексте того, что вы делаете, то вам будет очень не хватать этого языка, который его не поддерживает. В то же время, если команде это неудобно, или в этом нет реальной необходимости, и люди используют ее «только потому, что могут», тогда это контрпродуктивно.
Но нет, не существует убедительного, абсолютно верного аргумента, доказывающего, что множественное наследование является плохой идеей.
РЕДАКТИРОВАТЬ
Ответы на этот вопрос кажутся единодушными. Ради того, чтобы быть защитником дьявола, я приведу хороший пример множественного наследования, где невыполнение ведет к взлому.
Предположим, вы разрабатываете приложение для рынков капитала. Вам нужна модель данных для ценных бумаг. Некоторые ценные бумаги являются акциями (акции, трасты инвестиций в недвижимость и т. Д.), Другие являются долгами (облигации, корпоративные облигации), другие являются производными инструментами (опционы, фьючерсы). Поэтому, если вы избегаете MI, вы создадите очень простое и понятное дерево наследования. Акции унаследуют акции, облигации унаследуют долги. Отлично, но как насчет производных? Они могут быть основаны на продуктах, подобных акциям, или продуктах, подобных дебету? Хорошо, я думаю, что мы сделаем нашу ветку наследования больше. Имейте в виду, что некоторые деривативы основаны на продуктах акций, долговых продуктах или нет. Таким образом, наше наследственное дерево становится сложным. Затем приходит бизнес-аналитик и говорит вам, что теперь мы поддерживаем индексированные ценные бумаги (опционы на индекс, опционы на индексирование в будущем). И эти вещи могут быть основаны на справедливости, долге или производных. Это становится грязным! Получает ли моя будущая опция индекса Equity-> Stock-> Option-> Index? Почему бы не Эквити-> Фондовая-> Индекс-> Опция? Что если однажды я найду оба в своем коде (это случилось; правдивая история)?
Проблема здесь состоит в том, что эти фундаментальные типы могут быть смешаны в любой перестановке, которая не является естественным производным одного от другого. Объекты определяются по является зависимостью, поэтому композиция не имеет никакого смысла. Множественное наследование (или похожая концепция миксинов) является единственным логическим представлением здесь.
Реальное решение этой проблемы - определить типы Equity, Debt, Derivative, Index и смешать их, используя множественное наследование для создания модели данных. Это создаст объекты, которые имеют смысл и могут быть легко использованы для повторного использования кода.
источник
Equity
иDebt
оба реализуютISecurity
.Derivative
имеетISecurity
свойство. Это может быть само по себе,ISecurity
если уместно (я не знаю финансов).IndexedSecurities
снова содержит свойство интерфейса, которое применяется к типам, от которых ему разрешено основываться. Если они всеISecurity
, то все они имеютISecurity
свойства и могут быть произвольно вложены ...Другие ответы здесь, кажется, в основном попадают в теорию. Итак, вот конкретный пример Python, упрощенный, в который я действительно врезался, что потребовало значительного количества рефакторинга:
Bar
был написан, предполагая, что у него была своя реализацияzeta()
, что, как правило, довольно хорошее предположение. Подкласс должен переопределить его соответствующим образом, чтобы он делал правильные вещи. К сожалению, имена были только совпадением - они делали довольно разные вещи, ноBar
теперь вызывалиFoo
реализацию:Это довольно расстраивает, когда не выдается никаких ошибок, приложение начинает действовать очень незначительно, и изменение кода, которое вызвало его (создание
Bar.zeta
), кажется, не там, где проблема.источник
super()
?super()
получили вокругBang
кBar, Foo
также решить эту проблему - но ваша точка является хорошим, +1.Bar.zeta(self)
.Я бы сказал, что нет реальных проблем с MI на правильном языке . Ключ заключается в том, чтобы разрешить алмазные структуры, но требовать, чтобы подтипы обеспечивали свое собственное переопределение вместо того, чтобы компилятор выбирал одну из реализаций, основанную на некотором правиле.
Я делаю это на языке гуавы , на котором я работаю. Одной из особенностей Guava является то, что мы можем вызывать реализацию метода определенного супертипа. Таким образом, легко указать, какая реализация супертипа должна быть «унаследована» без какого-либо специального синтаксиса:
Если бы мы не дали
OrderedSet
егоtoString
, мы получили бы ошибку компиляции. Без сюрпризов.Я считаю, что MI особенно полезен для коллекций. Например, мне нравится использовать
RandomlyEnumerableSequence
тип, чтобы избежать объявленияgetEnumerator
массивов, запросов и т. Д.Если бы у нас не было MI, мы могли бы написать
RandomAccessEnumerator
для нескольких коллекций, но необходимость написать краткийgetEnumerator
метод все еще добавляет шаблон.Точно так же, MI полезно для наследования стандартных реализаций
equals
,hashCode
иtoString
для коллекций.источник
toString
поведении России, поэтому переопределение его поведения не нарушает принцип подстановки. Вы также иногда получаете «конфликты» между методами, которые имеют одинаковое поведение, но разные алгоритмы, особенно с коллекциями.Ordered extends Set and Sequence
ADiamond
? Это просто соединение. Ему не хватаетhat
вершины. Почему вы называете это бриллиантом? Я спрашивал здесь, но это кажется запретным вопросом. Как вы узнали, что нужно называть эту триангулярную структуру Бриллиантом, а не Присоединением?Top
тип, который объявляетtoString
, таким образом, была фактически структура алмаза. Но я думаю, что у вас есть смысл - «соединение» без алмазной структуры создает аналогичную проблему, и большинство языков обрабатывают оба случая одинаково.Наследование, множественное или иное, не так важно. Если два объекта различного типа являются заменяемыми, это имеет значение, даже если они не связаны наследованием.
Связанный список и строка символов имеют мало общего и не должны быть связаны наследованием, но это полезно, если я могу использовать
length
функцию для получения количества элементов в одном из них.Наследование - это хитрость, позволяющая избежать повторной реализации кода. Если наследование спасает вас от работы, а множественное наследование экономит вам еще больше работы по сравнению с единичным наследованием, то это все необходимое обоснование.
Я подозреваю, что некоторые языки не очень хорошо реализуют множественное наследование, и для практиков этих языков именно это означает множественное наследование. Упомяните множественное наследование программисту на C ++, и на ум приходят вопросы о проблемах, когда класс заканчивается двумя копиями базы по двум разным путям наследования, а также о том, следует ли использовать
virtual
в базовом классе, и путанице в том, как называются деструкторы , и так далее.Во многих языках наследование класса ассоциируется с наследованием символов. Когда вы выводите класс D из класса B, вы не только создаете отношение типа, но и потому, что эти классы также служат лексическими пространствами имен, вы имеете дело с импортом символов из пространства имен B в пространство имен D, в дополнение к семантика того, что происходит с самими типами B и D. Поэтому множественное наследование приводит к проблемам столкновения символов. Если мы наследуем от
card_deck
иgraphic
, оба из которых «имеют»draw
метод, что это значит дляdraw
результирующего объекта? Объектная система, у которой нет этой проблемы, - это система в Common Lisp. Возможно, не случайно множественное наследование используется в программах на Лиспе.Плохо реализовано, неудобно все (например, множественное наследование) должно быть ненавистно.
источник
length
операции полезна независимо от концепции наследования (что в данном случае не помогает: мы вряд ли сможем достичь чего-либо, пытаясь разделить реализацию между два методаlength
функции). Тем не менее, может существовать некоторое абстрактное наследование: например, и список, и строка имеют последовательность типов (но которая не обеспечивает никакой реализации).Насколько я могу судить, отчасти проблема (помимо того, что ваш дизайн немного сложнее для понимания (тем не менее, легче кодировать)) заключается в том, что компилятор сэкономит достаточно места для данных вашего класса, что позволит сделать это огромным количеством потеря памяти в следующем случае:
(Мой пример, возможно, не самый лучший, но попытайтесь понять суть нескольких областей памяти для той же цели, это было первое, что пришло мне в голову: P)
Считайте DDD, когда классная собака простирается от caninus и домашнего животного, caninus имеет переменную, которая указывает количество пищи, которую он должен съесть (целое число) под именем dietKg, но домашнее животное также имеет другую переменную для этой цели, обычно под той же самой name (если вы не установите другое имя переменной, то вам придется кодифицировать дополнительный код, который изначально был проблемой, которую вы хотели избежать, чтобы обрабатывать и сохранять целостность обоих переменных), тогда у вас будет два пространства памяти для точного с той же целью, чтобы избежать этого, вам придется изменить свой компилятор, чтобы он распознавал это имя в том же пространстве имен и просто назначал одно пространство памяти для этих данных, что, к сожалению, невозможно определить во время компиляции.
Вы, конечно, могли бы создать язык, чтобы указать, что такая переменная может уже иметь пространство, определенное где-то еще, но в конце программист должен указать, где находится пространство памяти, на которое ссылается эта переменная (и снова дополнительный код).
Поверьте мне, люди, которые очень усердно воплощают эту мысль во всем этом, но я рад, что вы спросили, ваш добрый взгляд - это тот, который меняет парадигмы;), и в заключение, я не говорю, что это невозможно (но многие предположения и должен быть реализован многофазный компилятор, и очень сложный), я просто говорю, что его еще не существует, если вы запускаете проект для своего собственного компилятора, способного выполнять «это» (множественное наследование), пожалуйста, дайте мне знать, я буду рад присоединиться к вашей команде.
источник
В течение долгого времени мне никогда не приходило в голову, насколько совершенно одни задачи программирования отличаются от других - и насколько это помогает, если используемые языки и шаблоны адаптированы к проблемному пространству.
Когда вы работаете в одиночку или в основном изолированы от написанного вами кода, это совсем другое проблемное пространство, чем унаследование кодовой базы от 40 человек в Индии, которые работали над ней в течение года, прежде чем передать ее вам без какой-либо помощи при переходе.
Представьте, что вы были наняты компанией вашей мечты, а затем унаследовали такую кодовую базу. Кроме того, представьте, что консультанты изучали наследование и множественное наследование (и, следовательно, увлекались им) ... Не могли бы вы представить, над чем вы работаете.
Когда вы наследуете код, самая важная особенность - это то, что он понятен, и части изолированы, поэтому они могут работать независимо. Конечно, когда вы впервые пишете такие структуры кода, как множественное наследование, это может сэкономить вам немного дублирования и, кажется, соответствует вашему логическому настроению в то время, но у следующего парня просто еще есть что распутать.
Каждое соединение в вашем коде также усложняет понимание и изменение частей независимо, вдвойне, с множественным наследованием.
Когда вы работаете как часть команды, вы хотите нацеливаться на самый простой из возможных кодов, который не дает вам абсолютно никакой избыточной логики (на самом деле это означает, что DRY не означает, что вы не должны много печатать, просто вам никогда не придется менять свой код в 2). места для решения проблемы!)
Существуют более простые способы получения кода DRY, чем множественное наследование, поэтому включение его в язык может открыть вас только для проблем, возникших у других людей, которые могут не иметь такого же уровня понимания, как вы. Это даже соблазнительно, если ваш язык не способен предложить вам простой / менее сложный способ сохранить ваш код СУХИМЫМ.
источник
Самый большой аргумент против множественного наследования заключается в том, что некоторые полезные способности могут быть предоставлены, а некоторые полезные аксиомы могут сохраняться в рамках, которые строго ограничивают его (*), но не могут быть предоставлены и / или не поддерживаться без таких ограничений. Из их:
Возможность иметь несколько отдельно скомпилированных модулей включает в себя классы, которые наследуются от классов других модулей, и перекомпилировать модуль, содержащий базовый класс, без необходимости перекомпиляции каждого модуля, который наследуется от этого базового класса.
Возможность для типа наследовать реализацию члена от родительского класса без необходимости повторной реализации производного типа
Аксиома, что любой экземпляр объекта может быть повышен или понижен непосредственно к себе или любому из его базовых типов, и такие повышающие и понижающие значения, в любой комбинации или последовательности, всегда сохраняют идентичность
Аксиома, что если производный класс переопределяет и связывает цепочку с элементом базового класса, базовый член будет вызываться напрямую посредством связующего кода и не будет вызываться где-либо еще.
(*) Обычно требуется, чтобы типы, поддерживающие множественное наследование, объявлялись как «интерфейсы», а не классы, и не позволяли интерфейсам делать все, что могут делать обычные классы.
Если кто-то хочет разрешить обобщенное множественное наследование, что-то еще должно уступить место. Если X и Y оба наследуют от B, они оба переопределяют один и тот же член M и цепочку к базовой реализации, и если D наследует от X и Y, но не переопределяет M, то с учетом экземпляра q типа D, что должно (( B) q) .M () делать? Запрещение такого приведения нарушит аксиому, согласно которой любой объект может быть преобразован в любой базовый тип, но любое возможное поведение приведения и приведения члена нарушит аксиому, касающуюся цепочки методов. Можно потребовать, чтобы классы загружались только в сочетании с конкретной версией базового класса, против которого они были скомпилированы, но это часто неудобно. Наличие во время выполнения отказа от загрузки любого типа, в котором любой предок может быть достигнут более чем одним маршрутом, может быть работоспособным, но сильно ограничит полезность множественного наследования. Разрешение общих путей наследования только при отсутствии конфликтов может привести к ситуациям, когда старая версия X будет совместима со старым или новым Y, а старый Y будет совместим со старым или новым X, но новый X и новый Y будут быть совместимым, даже если ни один из них не сделал ничего такого, что само по себе должно было бы привести к серьезным изменениям.
Некоторые языки и структуры допускают множественное наследование, согласно теории, что то, что получено от MI, более важно, чем то, что должно быть отказано, чтобы это позволить. Однако затраты на MI значительны, и во многих случаях интерфейсы обеспечивают 90% преимуществ MI при небольшой доле затрат.
источник
FirstDerivedFoo
переопределениеBar
ничего не делает, кроме цепочки с защищенным не виртуальнымFirstDerivedFoo_Bar
, иSecondDerivedFoo
переопределение цепочки с защищенным не виртуальнымSecondDerivedBar
, и если код, который хочет получить доступ к базовому методу, использует эти защищенные не виртуальные методы, то может быть подходящим подходом ...base.VirtualMethod
), но я не знаю какой-либо языковой поддержки, которая бы особенно облегчала такой подход. Хотелось бы, чтобы был чистый способ присоединить один блок кода как к не виртуальному защищенному методу, так и к виртуальному методу с одной и той же сигнатурой, не требуя виртуального метода с «одной строкой» (который в итоге занимает намного больше одной строки в исходном коде).