Существует ли какая-либо «настоящая» причина множественного наследования?

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

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

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

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

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

Пример

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

  1. Потому что он - это электронный. Наследование или композиция всегда должны быть отношением is-a против отношения has-a.

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

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

Почему бы не использовать интерфейсы? Возьмите # 3, например. Я не хочу писать это снова и снова, и я действительно не хочу создавать какой-то причудливый вспомогательный класс прокси для этого:

 private void runOrDont()
{
    if (this.battery)
    {
        if (this.battery.working && this.switchedOn)
        {
            this.run();
            return;
        }
    }
    this.dontRun();
}

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

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

118 голосов | спросил Panzercrisis 14 42013vEurope/Moscow11bEurope/MoscowThu, 14 Nov 2013 19:59:37 +0400 2013, 19:59:37

9 ответов


66

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

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

Некоторые языки решили сделать черты явной конструкцией внутри языка. Другие осторожно направляют вас в этом направлении, удаляя 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 и Rectangle /Square, являются результатом неопытного дизайна для классов. В принципе избежать множественного наследования - это способ помочь начинающим разработчикам избежать стрельбы в ногу. Как и все принципы проектирования, наличие дисциплины и обучения, основанных на них, позволяет вам со временем обнаружить, когда с ними можно отрываться. См. Модель использования навыка Dreyfus , на уровне Эксперта, ваше внутреннее знание выходит за рамки максим /принципы. Вы можете «чувствовать», когда правило не применяется.

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

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

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

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

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

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

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

ответил Michael Brown 14 42013vEurope/Moscow11bEurope/MoscowThu, 14 Nov 2013 20:41:39 +0400 2013, 20:41:39
59
  

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

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

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

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

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

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

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

ответил Telastyn 14 42013vEurope/Moscow11bEurope/MoscowThu, 14 Nov 2013 20:25:29 +0400 2013, 20:25:29
29

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

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

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

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

ИЗМЕНИТЬ

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

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

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

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

ответил MrFox 14 42013vEurope/Moscow11bEurope/MoscowThu, 14 Nov 2013 23:03:22 +0400 2013, 23:03:22
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), похоже, не является где проблема лежит.

ответил Izkata 15 52013vEurope/Moscow11bEurope/MoscowFri, 15 Nov 2013 00:00:06 +0400 2013, 00:00:06
8

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

Я делаю это в Guava , языке, над которым я работаю. Одна из особенностей 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 для коллекций.

ответил Daniel Lubarov 15 52013vEurope/Moscow11bEurope/MoscowFri, 15 Nov 2013 09:45:52 +0400 2013, 09:45:52
7

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

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

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

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

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

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

ответил Kaz 14 42013vEurope/Moscow11bEurope/MoscowThu, 14 Nov 2013 23:39:57 +0400 2013, 23:39:57
1

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

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

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

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

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

ответил Ordiel 14 42013vEurope/Moscow11bEurope/MoscowThu, 14 Nov 2013 20:52:46 +0400 2013, 20:52:46
-1

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

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

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

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

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

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

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

ответил Bill K 16 62013vEurope/Moscow11bEurope/MoscowSat, 16 Nov 2013 08:22:43 +0400 2013, 08:22:43
-3

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

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

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

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

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

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

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

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

ответил supercat 15 52013vEurope/Moscow11bEurope/MoscowFri, 15 Nov 2013 02:14:34 +0400 2013, 02:14:34

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132