Есть ли «реальная» причина, по которой множественное наследование ненавидят?


123

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

Единственная основная причина, которую я когда-либо слышал для этого, - проблема алмазов с базовыми классами. Я просто не могу принять это. Для меня это звучит ужасно, например: «Ну, это возможно испортить, так что это автоматически плохая идея». Вы можете испортить что угодно на языке программирования, и я имею в виду все, что угодно. Я просто не могу принять это всерьез, по крайней мере, без более подробного объяснения.

Просто осознание этой проблемы - это 90% битвы. Кроме того, я думаю, что слышал что-то много лет назад о обходном пути общего назначения, включающем алгоритм «конверта» или что-то в этом роде (кто-нибудь звонит в колокол?).

Что касается проблемы с бриллиантами, единственная потенциально подлинная проблема, о которой я могу подумать, это если вы пытаетесь использовать стороннюю библиотеку и не видите, что два, казалось бы, не связанных класса в этой библиотеке имеют общий базовый класс, но в дополнение к документация, простая языковая функция, скажем, может потребовать, чтобы вы специально заявили о своем намерении создать бриллиант, прежде чем он фактически скомпилирует его для вас. С такой особенностью любое создание алмаза является либо преднамеренным, безрассудным, либо потому, что человек не знает об этой ловушке.

Так что все сказанное ... Есть ли какая-то реальная причина, по которой большинство людей ненавидят множественное наследование, или это просто кучка истерии, которая приносит больше вреда, чем пользы? Есть ли что-то, чего я здесь не вижу? Спасибо.

пример

Автомобиль расширяет WheeledVehicle, KIASpectra расширяет Car и Electronic, KIASpectra содержит радио. Почему KIASpectra не содержит Electronic?

  1. Потому что это Электронный. Наследование против состава всегда должно быть отношением «есть» или «иметь».

  2. Потому что это Электронный. Есть провода, платы, переключатели и т. Д. Все это вверх и вниз по этой вещи.

  3. Потому что это Электронный. Если ваша батарея разряжается зимой, вы попадаете в такую же проблему, как если бы все ваши колеса внезапно пропали без вести.

Почему бы не использовать интерфейсы? Взять, к примеру, № 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, или о более чистом примере.


24
это также поощряет наследование по составу ... (когда это должно быть наоборот)
трещотка урод

34
«Вы можете испортить это» - совершенно веская причина для удаления функции. В этом вопросе современный дизайн языков несколько расщеплен, но вы можете в целом классифицировать языки на «Власть от ограничений» и «Власть от гибкости». Ничего из этого не является «правильным», я не думаю, что у них обоих есть сильные стороны. МИ - это одна из тех вещей, которая чаще всего используется для Зла, и поэтому ограничивающие языки удаляют его. Гибкие не делают, потому что «чаще, чем нет» не «буквально всегда». Тем не менее, я думаю, что миксин / черты являются лучшим решением для общего случая.
Phoshi

8
Существуют более безопасные альтернативы множественному наследованию в нескольких языках, которые выходят за рамки интерфейсов. Проверьте Scala Traits- они действуют как интерфейсы с необязательной реализацией, но имеют некоторые ограничения, которые помогают предотвратить возникновение таких проблем, как проблема с бриллиантами.
KChaloux

5
См. Также этот ранее закрытый вопрос: programmers.stackexchange.com/questions/122480/…
Док Браун

19
KiaSpectraне является Electronic ; у него есть электроника, и он может быть ElectronicCar(который будет расширяться Car...)
Брайан С.

Ответы:


68

Во многих случаях люди используют наследование, чтобы предоставить черту классу. Например, подумайте о Пегасе. При множественном наследовании у вас может возникнуть соблазн сказать, что Пегас расширяет Лошадь и Птицу, потому что вы классифицировали Птицу как животное с крыльями.

Однако у Птиц есть и другие черты, которых нет у Пегаси. Например, птицы откладывают яйца, у пегасов есть живорождение. Если наследование является вашим единственным средством передачи общих черт, то нет способа исключить черту яйцекладки из Пегаса.

Некоторые языки решили сделать черты явной конструкцией внутри языка. Другие мягко направляют вас в этом направлении, удаляя MI из языка. В любом случае, я не могу вспомнить ни одного случая, когда я подумал: «Чувак, мне действительно нужен МИ, чтобы сделать это правильно».

Также давайте обсудим, что такое НАСТОЯЩЕЕ наследование. Когда вы наследуете от класса, вы берете зависимость от этого класса, но также вы должны поддерживать контракты, которые поддерживает класс, как неявные, так и явные.

Возьмите классический пример квадрата, наследуемого от прямоугольника. Прямоугольник предоставляет свойство длины и ширины, а также методы getPerimeter и getArea. Квадрат переопределяет длину и ширину, поэтому, когда один из них установлен, другой устанавливается так, чтобы он соответствовал getPerimeter и getArea работал бы одинаково (2 * длина + 2 * ширина для периметра и длина * ширина для области).

Существует один тестовый случай, который ломается, если вы замените эту реализацию квадрата на прямоугольник.

var rectangle = new Square();
rectangle.length= 5;
rectangle.width= 6;
Assert.AreEqual(30, rectangle.GetArea()); 
//Square returns 36 because setting the width clobbers the length

Достаточно сложно разобраться с единой цепочкой наследования. Это становится еще хуже, когда вы добавляете еще один в микс.


Подводные камни, о которых я упоминал в Pegasus в MI, и отношения «Прямоугольник / Квадрат» - оба являются результатом неопытного дизайна для классов. По сути, избегание множественного наследования - это способ помочь начинающим разработчикам избежать стрельбы в ногу. Как и все принципы проектирования, дисциплина и основанная на них подготовка позволяют вовремя обнаружить, когда можно оторваться от них. См. Модель приобретения навыков Дрейфусом , на уровне эксперта ваши внутренние знания выходят за рамки принципов / принципов. Вы можете «чувствовать», когда правило не применяется.

И я согласен с тем, что я несколько обманул пример «реального мира», почему MI не одобряется.

Давайте посмотрим на структуру пользовательского интерфейса. В частности, давайте рассмотрим несколько виджетов, которые на первый взгляд могут выглядеть так, будто они представляют собой просто комбинацию двух других. Как ComboBox. ComboBox - это TextBox, поддерживающий DropDownList. Т.е. я могу ввести значение или выбрать из заранее определенного списка значений. Наивным подходом было бы наследовать ComboBox от TextBox и DropDownList.

Но ваше текстовое поле получает свою ценность от того, что набрал пользователь. В то время как DDL получает свое значение от того, что выбирает пользователь. Кто берет прецедент? DDL, возможно, был разработан для проверки и отклонения любого ввода, которого не было в его первоначальном списке значений. Мы отвергаем эту логику? Это означает, что мы должны предоставить внутреннюю логику для переопределения наследников. Или, что еще хуже, добавьте логику в базовый класс, который существует только для поддержки подкласса (нарушая принцип инверсии зависимости ).

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

Например, Grid - это панель макета в WPF, к которой прикреплены свойства Column и Row, которые указывают, где дочерний элемент должен быть размещен в расположении сетки. Без вложенных свойств, если я хочу расположить кнопку в сетке, кнопка должна быть производной от сетки, чтобы иметь доступ к свойствам столбца и строки.

Разработчики развили эту концепцию и использовали присоединенные свойства как способ компоновки поведения (например, вот мой пост о создании сортируемого GridView с использованием вложенных свойств, написанных до того, как WPF включил DataGrid). Подход был признан в качестве шаблона проектирования XAML под названием « Присоединенное поведение» .

Надеюсь, это дало немного больше понимания того, почему множественное наследование обычно не одобряется.


14
«Чувак, мне действительно нужен МИ, чтобы сделать это правильно», - см. Мой ответ для примера. Короче говоря, ортогональные концепции, которые удовлетворяют отношениям is-a, имеют смысл для MI. Они очень редки, но они существуют.
MrFox

13
Чувак, я ненавижу этот пример прямоугольника. Есть много более ярких примеров, которые не требуют изменчивости.
asmeurer

9
Мне нравится, что ответом «предоставить реальный пример» было обсуждение вымышленного существа. Отличная ирония +1
jjathman

3
Таким образом, вы говорите, что множественное наследование является плохим, потому что наследование является плохим (вы все примеры о наследовании одного интерфейса). Поэтому, основываясь только на этом ответе, язык должен либо избавляться от наследования и интерфейсов, либо иметь множественное наследование.
Ctrl-Alt-Delor

12
Я не думаю, что эти примеры действительно работают ... никто не говорит, что пегас - это птица и лошадь ... вы говорите, что это крылатый животный и копытный животный. Пример с квадратом и прямоугольником имеет немного больше смысла, потому что вы обнаруживаете, что объект ведет себя не так, как вы думаете на основании его утвержденного определения. Тем не менее, я думаю, что эта ситуация была бы ошибкой программистов за то, что они этого не поймали (если это действительно оказалось проблемой).
Ричард

60

Есть ли что-то, чего я здесь не вижу?

Разрешение множественного наследования делает правила о перегрузках функций и виртуальной диспетчеризации, безусловно, более сложными, а также реализацию языка вокруг макетов объектов. Это немного влияет на разработчиков / разработчиков языка и поднимает и без того высокую планку, чтобы сделать язык стабильным и приемлемым.

Другой распространенный аргумент, который я видел (и приводил время от времени), состоит в том, что имея два + базовых класса, ваш объект почти всегда нарушает принцип единой ответственности. Либо два + базовых класса являются хорошими автономными классами со своей собственной ответственностью (вызывающей нарушение), либо они являются частичными / абстрактными типами, которые работают друг с другом, создавая единую связную ответственность.

В этом другом случае у вас есть 3 сценария:

  1. A ничего не знает о B - Отлично, вы можете объединить занятия, потому что вам повезло.
  2. A знает о B - Почему A просто не унаследовал от B?
  3. А и Б знают друг о друге - Почему ты просто не сделал один урок? Какая выгода от того, чтобы сделать эти вещи такими связанными, но частично заменяемыми?

Лично я думаю, что множественное наследование имеет плохой рэп, и что хорошо сделанная система композиции стилей черт была бы действительно мощной / полезной ... но есть много способов, которыми она может быть реализована плохо, и по многим причинам это не очень хорошая идея в таком языке, как C ++.

[править] в отношении вашего примера, это абсурд. А у Киа есть электроника. У него есть двигатель. Точно так же у электроники есть источник питания, который просто оказывается автомобильным аккумулятором. Наследование, не говоря уже о множественном наследовании, там не место.


8
Другой интересный вопрос - как инициализировать базовые классы + их базовые классы? Инкапсуляция> Наследование.
Jozefg

«хорошо сделано система признака состава стиля будет действительно мощной / полезной ...» - Это известно как Mixins , и они являются мощными / полезными. Миксины могут быть реализованы с использованием MI, но множественное наследование не требуется для миксинов. Некоторые языки поддерживают миксины изначально, без MI.
BlueRaja - Дэнни Пфлугхофт

В частности, для Java у нас уже есть несколько интерфейсов и INVOKEINTERFACE, поэтому у MI не будет никаких очевидных последствий для производительности.
Даниэль Любаров

Tetastyn, я согласен, что пример был плохим и не служил его вопросу. Наследование не для прилагательных (электронное), это для существительных (транспортное средство).
Ричард

29

Единственная причина, по которой он запрещен, заключается в том, что людям легко выстрелить себе в ногу.

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

Если ваши разработчики знакомы с MI, и MI имеет смысл в контексте того, что вы делаете, то вам будет очень не хватать этого языка, который его не поддерживает. В то же время, если команде это неудобно, или в этом нет реальной необходимости, и люди используют ее «только потому, что могут», тогда это контрпродуктивно.

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

РЕДАКТИРОВАТЬ

Ответы на этот вопрос кажутся единодушными. Ради того, чтобы быть защитником дьявола, я приведу хороший пример множественного наследования, где невыполнение ведет к взлому.

Предположим, вы разрабатываете приложение для рынков капитала. Вам нужна модель данных для ценных бумаг. Некоторые ценные бумаги являются акциями (акции, трасты инвестиций в недвижимость и т. Д.), Другие являются долгами (облигации, корпоративные облигации), другие являются производными инструментами (опционы, фьючерсы). Поэтому, если вы избегаете MI, вы создадите очень простое и понятное дерево наследования. Акции унаследуют акции, облигации унаследуют долги. Отлично, но как насчет производных? Они могут быть основаны на продуктах, подобных акциям, или продуктах, подобных дебету? Хорошо, я думаю, что мы сделаем нашу ветку наследования больше. Имейте в виду, что некоторые деривативы основаны на продуктах акций, долговых продуктах или нет. Таким образом, наше наследственное дерево становится сложным. Затем приходит бизнес-аналитик и говорит вам, что теперь мы поддерживаем индексированные ценные бумаги (опционы на индекс, опционы на индексирование в будущем). И эти вещи могут быть основаны на справедливости, долге или производных. Это становится грязным! Получает ли моя будущая опция индекса Equity-> Stock-> Option-> Index? Почему бы не Эквити-> Фондовая-> Индекс-> Опция? Что если однажды я найду оба в своем коде (это случилось; правдивая история)?

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

Реальное решение этой проблемы - определить типы Equity, Debt, Derivative, Index и смешать их, используя множественное наследование для создания модели данных. Это создаст объекты, которые имеют смысл и могут быть легко использованы для повторного использования кода.


27
Я работал в сфере финансов. Есть причина, по которой функциональное программирование в финансовом программном обеспечении предпочтительнее ОО. И ты указал на это. МИ не решает эту проблему, она усугубляет ее. Поверьте мне, я не могу сказать вам, сколько раз я слышал «Это так… кроме», когда имеешь дело с финансовыми клиентами. Это только становится проклятием моего существования.
Майкл Браун

8
Мне кажется, что это очень хорошо решается с интерфейсами ... на самом деле, кажется, что он лучше подходит для интерфейсов, чем все остальное. Equityи Debtоба реализуют ISecurity. Derivativeимеет ISecurityсвойство. Это может быть само по себе, ISecurityесли уместно (я не знаю финансов). IndexedSecuritiesснова содержит свойство интерфейса, которое применяется к типам, от которых ему разрешено основываться. Если они все ISecurity, то все они имеют ISecurityсвойства и могут быть произвольно вложены ...
Бобсон,

11
@ Бобсон: проблема заключается в том, что вам нужно заново реализовать интерфейс для каждого разработчика, и во многих случаях он одинаковый. Вы можете использовать делегирование / состав, но тогда вы потеряете доступ к частным пользователям. А поскольку в Java нет делегатов / лямбд ... буквально нет хорошего способа сделать это. За исключением, возможно, с таким инструментом Aspect, как Aspect4J
Майкл Браун

10
@ Бобсон именно то, что сказал Майк Браун. Да, вы можете разработать решение с помощью интерфейсов, но оно будет неуклюжим. Ваша интуиция в использовании интерфейсов очень верна, но это скрытое желание смешивать / множественное наследование :).
MrFox

11
Это касается странности, когда ОО-программисты предпочитают мыслить с точки зрения наследования и часто не принимают делегирование в качестве допустимого ОО-подхода, который обычно дает более простое, более обслуживаемое и расширяемое решение. Работая в сфере финансов и здравоохранения, я видел неуправляемый беспорядок, который может создать МИ, особенно в связи с тем, что законы о налогах и здравоохранении меняются с каждым годом, а определение объекта ПОСЛЕДНИЙ год недействительно для ЭТОГО года (но все равно должно выполнять функцию любого года и должны быть взаимозаменяемыми). Композиция дает более тонкий код, который легче тестировать, и он стоит меньше с течением времени.
Шон Уилсон

11

Другие ответы здесь, кажется, в основном попадают в теорию. Итак, вот конкретный пример Python, упрощенный, в который я действительно врезался, что потребовало значительного количества рефакторинга:

class Foo(object):
   def zeta(self):
      print "foozeta"

class Bar(object):
   def zeta(self):
      print "barzeta"

   def barstuff(self):
      print "barstuff"
      self.zeta()

class Bang(Foo, Bar):
   def stuff(self):
      self.zeta()
      print "---"
      self.barstuff()

z = Bang()
z.stuff()

Barбыл написан, предполагая, что у него была своя реализация zeta(), что, как правило, довольно хорошее предположение. Подкласс должен переопределить его соответствующим образом, чтобы он делал правильные вещи. К сожалению, имена были только совпадением - они делали довольно разные вещи, но Barтеперь вызывали Fooреализацию:

foozeta
---
barstuff
foozeta

Это довольно расстраивает, когда не выдается никаких ошибок, приложение начинает действовать очень незначительно, и изменение кода, которое вызвало его (создание Bar.zeta), кажется, не там, где проблема.


Как избежать алмазной проблемы с помощью звонков super()?
Panzercrisis

@Panzercrisis Извините, не обращайте внимания на эту часть. Я misremembered, задача которого «проблема алмаз» обычно относится к - Python был отдельный вопрос , вызванный ромбовидным наследованием , которые super()получили вокруг
Izkata

5
Я рад, что MI в C ++ гораздо лучше спроектирован. Вышеуказанная ошибка просто невозможна. Вы должны вручную устранить неоднозначность, прежде чем код даже скомпилируется.
Томас Эдинг

2
Изменение порядка наследования в Bangк Bar, Fooтакже решить эту проблему - но ваша точка является хорошим, +1.
Шон Виейра

1
@ThomasEding Вы можете вручную неоднозначность еще: Bar.zeta(self).
Тайлер Кромптон

9

Я бы сказал, что нет реальных проблем с MI на правильном языке . Ключ заключается в том, чтобы разрешить алмазные структуры, но требовать, чтобы подтипы обеспечивали свое собственное переопределение вместо того, чтобы компилятор выбирал одну из реализаций, основанную на некотором правиле.

Я делаю это на языке гуавы , на котором я работаю. Одной из особенностей Guava является то, что мы можем вызывать реализацию метода определенного супертипа. Таким образом, легко указать, какая реализация супертипа должна быть «унаследована» без какого-либо специального синтаксиса:

type Sequence[+A] {
  String toString() {
    return "[" + ... + "]";
  }
}

type Set[+A] {
  String toString() {
    return "{" + ... + "}";
  }
}

type OrderedSet[+A] extends Sequence[A], Set[A] {
  String toString() {
    // This is Guava's syntax for statically invoking instance methods
    return Set.toString(this);
  }
}

Если бы мы не дали OrderedSetего toString, мы получили бы ошибку компиляции. Без сюрпризов.

Я считаю, что MI особенно полезен для коллекций. Например, мне нравится использовать RandomlyEnumerableSequenceтип, чтобы избежать объявления getEnumeratorмассивов, запросов и т. Д.

type Enumerable[+A] {
  Source[A] getEnumerator();
}

type Sequence[+A] extends Enumerable[A] {
  A get(Int index);
}

type RandomlyEnumerableSequence[+A] extends Sequence[A] {
  Source[A] getEnumerator() {
    ...
  }
}

type DynamicArray[A] extends MutableStack[A],
                             RandomlyEnumerableSequence[A] {
  // No need to define getEnumerator.
}

Если бы у нас не было MI, мы могли бы написать RandomAccessEnumeratorдля нескольких коллекций, но необходимость написать краткий getEnumeratorметод все еще добавляет шаблон.

Точно так же, MI полезно для наследования стандартных реализаций equals, hashCodeи toStringдля коллекций.


1
@AndyzSmith, я понимаю вашу точку зрения, но не все конфликты наследования являются актуальными семантическими проблемами. Рассмотрим мой пример - ни один из типов не дает обещаний о toStringповедении России, поэтому переопределение его поведения не нарушает принцип подстановки. Вы также иногда получаете «конфликты» между методами, которые имеют одинаковое поведение, но разные алгоритмы, особенно с коллекциями.
Даниэль Любаров

1
@Asmageddon согласился, мои примеры касаются только функциональности. В чем вы видите преимущества миксинов? По крайней мере, в Scala черты - это, по сути, просто абстрактные классы, которые позволяют использовать MI - они все еще могут использоваться для идентификации и по-прежнему приводить к проблеме алмазов. Есть ли другие языки, в которых миксины имеют более интересные различия?
Даниэль Любаров

1
Технически абстрактные классы могут использоваться в качестве интерфейсов, для меня преимущество состоит в том, что сама конструкция является «подсказкой по использованию», что означает, что я знаю, чего ожидать, когда вижу код. Тем не менее, в настоящее время я пишу код на Lua, у которого нет встроенной модели ООП - существующие системы классов обычно позволяют включать таблицы в качестве миксинов, а в той, которую я кодирую, классы используются в качестве миксинов. Единственное отличие состоит в том, что вы можете использовать только (одиночное) наследование для идентификации, миксины для функциональности (без информации об идентичности) и интерфейсы для дальнейших проверок. Может быть, это не так уж и лучше, но для меня это имеет смысл.
Лламагеддон

2
Почему вы называете Ordered extends Set and SequenceA Diamond? Это просто соединение. Ему не хватает hatвершины. Почему вы называете это бриллиантом? Я спрашивал здесь, но это кажется запретным вопросом. Как вы узнали, что нужно называть эту триангулярную структуру Бриллиантом, а не Присоединением?
Val

1
@RecognizeEvilasWaste, что у языка есть неявный Topтип, который объявляет toString, таким образом, была фактически структура алмаза. Но я думаю, что у вас есть смысл - «соединение» без алмазной структуры создает аналогичную проблему, и большинство языков обрабатывают оба случая одинаково.
Даниэль Любаров

7

Наследование, множественное или иное, не так важно. Если два объекта различного типа являются заменяемыми, это имеет значение, даже если они не связаны наследованием.

Связанный список и строка символов имеют мало общего и не должны быть связаны наследованием, но это полезно, если я могу использовать lengthфункцию для получения количества элементов в одном из них.

Наследование - это хитрость, позволяющая избежать повторной реализации кода. Если наследование спасает вас от работы, а множественное наследование экономит вам еще больше работы по сравнению с единичным наследованием, то это все необходимое обоснование.

Я подозреваю, что некоторые языки не очень хорошо реализуют множественное наследование, и для практиков этих языков именно это означает множественное наследование. Упомяните множественное наследование программисту на C ++, и на ум приходят вопросы о проблемах, когда класс заканчивается двумя копиями базы по двум разным путям наследования, а также о том, следует ли использовать virtualв базовом классе, и путанице в том, как называются деструкторы , и так далее.

Во многих языках наследование класса ассоциируется с наследованием символов. Когда вы выводите класс D из класса B, вы не только создаете отношение типа, но и потому, что эти классы также служат лексическими пространствами имен, вы имеете дело с импортом символов из пространства имен B в пространство имен D, в дополнение к семантика того, что происходит с самими типами B и D. Поэтому множественное наследование приводит к проблемам столкновения символов. Если мы наследуем от card_deckи graphic, оба из которых «имеют» drawметод, что это значит для drawрезультирующего объекта? Объектная система, у которой нет этой проблемы, - это система в Common Lisp. Возможно, не случайно множественное наследование используется в программах на Лиспе.

Плохо реализовано, неудобно все (например, множественное наследование) должно быть ненавистно.


2
Ваш пример с методом length () для списков и контейнеров строк плохой, так как эти два делают совершенно разные вещи. Цель наследования не состоит в том, чтобы уменьшить повторение кода. Если вы хотите уменьшить количество повторений кода, вы преобразовываете общие части в новый класс.
BЈовић

1
@ BЈовић Они совсем не делают "совершенно" разные вещи.
Каз

1
Сравнивая строку и список , можно увидеть много повторений. Использование наследования для реализации этих распространенных методов было бы просто неправильно.
BЈовић

1
@ BЈовић В ответе не сказано, что строка и список должны быть связаны наследованием; наоборот. Возможность замены списка или строки в lengthоперации полезна независимо от концепции наследования (что в данном случае не помогает: мы вряд ли сможем достичь чего-либо, пытаясь разделить реализацию между два метода lengthфункции). Тем не менее, может существовать некоторое абстрактное наследование: например, и список, и строка имеют последовательность типов (но которая не обеспечивает никакой реализации).
Каз

2
Кроме того, наследование является способом рефакторинга частей в общий класс, а именно в базовый класс.
Каз

1

Насколько я могу судить, отчасти проблема (помимо того, что ваш дизайн немного сложнее для понимания (тем не менее, легче кодировать)) заключается в том, что компилятор сэкономит достаточно места для данных вашего класса, что позволит сделать это огромным количеством потеря памяти в следующем случае:

(Мой пример, возможно, не самый лучший, но попытайтесь понять суть нескольких областей памяти для той же цели, это было первое, что пришло мне в голову: P)

Считайте DDD, когда классная собака простирается от caninus и домашнего животного, caninus имеет переменную, которая указывает количество пищи, которую он должен съесть (целое число) под именем dietKg, но домашнее животное также имеет другую переменную для этой цели, обычно под той же самой name (если вы не установите другое имя переменной, то вам придется кодифицировать дополнительный код, который изначально был проблемой, которую вы хотели избежать, чтобы обрабатывать и сохранять целостность обоих переменных), тогда у вас будет два пространства памяти для точного с той же целью, чтобы избежать этого, вам придется изменить свой компилятор, чтобы он распознавал это имя в том же пространстве имен и просто назначал одно пространство памяти для этих данных, что, к сожалению, невозможно определить во время компиляции.

Вы, конечно, могли бы создать язык, чтобы указать, что такая переменная может уже иметь пространство, определенное где-то еще, но в конце программист должен указать, где находится пространство памяти, на которое ссылается эта переменная (и снова дополнительный код).

Поверьте мне, люди, которые очень усердно воплощают эту мысль во всем этом, но я рад, что вы спросили, ваш добрый взгляд - это тот, который меняет парадигмы;), и в заключение, я не говорю, что это невозможно (но многие предположения и должен быть реализован многофазный компилятор, и очень сложный), я просто говорю, что его еще не существует, если вы запускаете проект для своего собственного компилятора, способного выполнять «это» (множественное наследование), пожалуйста, дайте мне знать, я буду рад присоединиться к вашей команде.


1
В какой момент компилятор может предположить, что переменные с одинаковыми именами из разных родительских классов безопасны для объединения? Какая у вас гарантия, что caninus.diet и pet.diet на самом деле служат одной и той же цели? Что бы вы сделали с функцией / методами с тем же именем?
Мистер Миндор

хахаха, я полностью согласен с тобой @ Mr.Mindor, это именно то, что я говорю в своем ответе, единственный способ, который мне приходит в голову, чтобы приблизиться к этому подходу, это прототипно-ориентированное программирование, но я не говорю, что это невозможно и именно поэтому я сказал, что во время компиляции должно быть сделано много предположений, и некоторые дополнительные «данные» / спецификации должны быть написаны программистом (тем не менее, это больше кодирования, что было исходной проблемой)
Ордиэль

-1

В течение долгого времени мне никогда не приходило в голову, насколько совершенно одни задачи программирования отличаются от других - и насколько это помогает, если используемые языки и шаблоны адаптированы к проблемному пространству.

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

Представьте, что вы были наняты компанией вашей мечты, а затем унаследовали такую ​​кодовую базу. Кроме того, представьте, что консультанты изучали наследование и множественное наследование (и, следовательно, увлекались им) ... Не могли бы вы представить, над чем вы работаете.

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

Каждое соединение в вашем коде также усложняет понимание и изменение частей независимо, вдвойне, с множественным наследованием.

Когда вы работаете как часть команды, вы хотите нацеливаться на самый простой из возможных кодов, который не дает вам абсолютно никакой избыточной логики (на самом деле это означает, что DRY не означает, что вы не должны много печатать, просто вам никогда не придется менять свой код в 2). места для решения проблемы!)

Существуют более простые способы получения кода DRY, чем множественное наследование, поэтому включение его в язык может открыть вас только для проблем, возникших у других людей, которые могут не иметь такого же уровня понимания, как вы. Это даже соблазнительно, если ваш язык не способен предложить вам простой / менее сложный способ сохранить ваш код СУХИМЫМ.


Кроме того, представьте, что консультанты узнали об этом - я видел так много не-MI языков (например, .net), где консультанты сходили с ума по интерфейсным DI и IoC, чтобы все это превратилось в непостижимо запутанный беспорядок. МИ не делает это хуже, вероятно, делает это лучше.
gbjbaanb

@gbjbaanb Вы правы, кому-то, кого не обожгли, легко испортить какую-либо функцию - на самом деле, это почти требование для изучения функции, которую вы испортили, чтобы понять, как ее лучше всего использовать. Мне немного любопытно, потому что вы, кажется, говорите, что отсутствие MI заставляет больше DI / IoC, чего я не видел - я бы не подумал, что код консультанта, который вы получили со всеми дерьмовыми интерфейсами / DI, был бы лучше также иметь целое сумасшедшее дерево наследования «многие ко многим», но это может быть моей неопытностью в МИ. У вас есть страница, которую я мог бы прочитать о замене DI / IoC на MI?
Билл К

-3

Самый большой аргумент против множественного наследования заключается в том, что некоторые полезные способности могут быть предоставлены, а некоторые полезные аксиомы могут сохраняться в рамках, которые строго ограничивают его (*), но не могут быть предоставлены и / или не поддерживаться без таких ограничений. Из их:

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

  • Возможность для типа наследовать реализацию члена от родительского класса без необходимости повторной реализации производного типа

  • Аксиома, что любой экземпляр объекта может быть повышен или понижен непосредственно к себе или любому из его базовых типов, и такие повышающие и понижающие значения, в любой комбинации или последовательности, всегда сохраняют идентичность

  • Аксиома, что если производный класс переопределяет и связывает цепочку с элементом базового класса, базовый член будет вызываться напрямую посредством связующего кода и не будет вызываться где-либо еще.

(*) Обычно требуется, чтобы типы, поддерживающие множественное наследование, объявлялись как «интерфейсы», а не классы, и не позволяли интерфейсам делать все, что могут делать обычные классы.

Если кто-то хочет разрешить обобщенное множественное наследование, что-то еще должно уступить место. Если X и Y оба наследуют от B, они оба переопределяют один и тот же член M и цепочку к базовой реализации, и если D наследует от X и Y, но не переопределяет M, то с учетом экземпляра q типа D, что должно (( B) q) .M () делать? Запрещение такого приведения нарушит аксиому, согласно которой любой объект может быть преобразован в любой базовый тип, но любое возможное поведение приведения и приведения члена нарушит аксиому, касающуюся цепочки методов. Можно потребовать, чтобы классы загружались только в сочетании с конкретной версией базового класса, против которого они были скомпилированы, но это часто неудобно. Наличие во время выполнения отказа от загрузки любого типа, в котором любой предок может быть достигнут более чем одним маршрутом, может быть работоспособным, но сильно ограничит полезность множественного наследования. Разрешение общих путей наследования только при отсутствии конфликтов может привести к ситуациям, когда старая версия X будет совместима со старым или новым Y, а старый Y будет совместим со старым или новым X, но новый X и новый Y будут быть совместимым, даже если ни один из них не сделал ничего такого, что само по себе должно было бы привести к серьезным изменениям.

Некоторые языки и структуры допускают множественное наследование, согласно теории, что то, что получено от MI, более важно, чем то, что должно быть отказано, чтобы это позволить. Однако затраты на MI значительны, и во многих случаях интерфейсы обеспечивают 90% преимуществ MI при небольшой доле затрат.


Будут ли избиратели утверждать, что комментируют? Я считаю, что мой ответ содержит информацию, отсутствующую на других языках, в отношении семантических преимуществ, которые могут быть получены путем запрета множественного наследования. Тот факт, что для разрешения множественного наследования потребовалось бы отказаться от других полезных вещей, был бы хорошим поводом для любого, кто ценит эти другие вещи больше, чем множественное наследование, чтобы противостоять ИМ.
суперкат

2
Проблемы с MI легко решаются или полностью исключаются с использованием тех же методов, которые вы использовали бы при одиночном наследовании. MI не препятствует ни одной из упомянутых вами способностей / аксиом, за исключением второго ... и если вы наследуете от двух классов, которые обеспечивают разные поведения для одного и того же метода, вы все равно должны решить эту двусмысленность. Но если вам придется это делать, вы, вероятно, уже злоупотребляете наследованием.
Чао

@cHao: если кто-то требует, чтобы производные классы переопределяли родительские методы, а не цепочку к ним (то же правило, что и интерфейсы), можно избежать проблем, но это довольно серьезное ограничение. Если использовать шаблон, в котором FirstDerivedFooпереопределение Barничего не делает, кроме цепочки с защищенным не виртуальным FirstDerivedFoo_Bar, и SecondDerivedFooпереопределение цепочки с защищенным не виртуальным SecondDerivedBar, и если код, который хочет получить доступ к базовому методу, использует эти защищенные не виртуальные методы, то может быть подходящим подходом ...
суперкат

... (отказываясь от синтаксиса base.VirtualMethod), но я не знаю какой-либо языковой поддержки, которая бы особенно облегчала такой подход. Хотелось бы, чтобы был чистый способ присоединить один блок кода как к не виртуальному защищенному методу, так и к виртуальному методу с одной и той же сигнатурой, не требуя виртуального метода с «одной строкой» (который в итоге занимает намного больше одной строки в исходном коде).
суперкат

В MI нет обязательного требования, чтобы классы не вызывали базовые методы, и никакой особой причины для этого не потребуется. Кажется немного странным обвинять МИ, учитывая, что проблема также влияет на СИ.
Чао
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.