Зачем использовать наследование вообще?

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

Мой вопрос заключается в следующем: поскольку вы можете выполнить что-либо с композицией объекта, которую вы можете с классическим наследованием, и поскольку классическое наследование очень часто злоупотребляют [1], и поскольку состав объектов дает вам гибкость для изменения времени выполнения объекта делегата, почему бы вы никогда не использовали классическое наследование?

Я могу понять, почему вы рекомендуете наследование на некоторых языках, таких как Java и С++, которые не предлагают удобный синтаксис для делегирования. На этих языках вы можете сэкономить много ввода, используя наследование, когда это явно не так. Но другие языки, такие как Objective C и Ruby, предлагают как классическое наследование, так и очень удобный синтаксис для делегирования. Я знаю, что язык программирования Go является единственным языком, который, насколько мне известно, решил, что классическое наследование больше проблем, чем того стоит, и поддерживает только делегирование для повторного использования кода.

Еще один способ изложить мой вопрос таков: даже если вы знаете, что классическое наследование не является неправильным для реализации определенной модели, является ли эта причина достаточной для ее использования вместо композиции?

[1] Многие люди используют классическое наследование для достижения полиморфизма вместо того, чтобы позволить своим классам реализовывать интерфейс. Цель наследования - повторное использование кода, а не полиморфизм. Кроме того, некоторые люди используют наследование, чтобы моделировать их интуитивное понимание отношения "is-a" которое часто может быть проблематичным.

Обновление

Я просто хочу уточнить, что я имею в виду, когда говорю о наследовании:

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

Обновление 2

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

Ответы

Ответ 1

Если вы делегируете все, что вы явно не переопределили для какого-либо другого объекта, реализующего один и тот же интерфейс ( "базовый" объект), тогда у вас есть основное преимущество Greenspunned поверх композиции, но (на большинстве языков) с много больше многословия и шаблона. Цель использования композиции вместо наследования заключается в том, что вы можете делегировать только поведение, которое вы хотите делегировать.

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

Ответ 2

Цель наследования - повторное использование кода, а не полиморфизм.

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

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

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

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

Ответ 3

Основная причина использования наследования не как форма композиции - это значит, что вы можете получить полиморфное поведение. Если вам не нужен полиморфизм, вы, вероятно, не должны использовать наследование, по крайней мере, на С++.

Ответ 4

Наследование предпочтительнее, если:

  • Вам нужно разоблачить весь API класса, который вы расширяете (с делегированием вам нужно будет написать много методов делегирования), и ваш язык не предлагает простой способ сказать "делегировать все неизвестные методы".
  • Вам необходимо получить доступ к защищенным полям/методам для языков, не имеющих понятия "друзья"
  • Преимущества делегирования несколько снижаются, если ваш язык допускает многоуровневое управление
  • У вас обычно нет необходимости в деле делегирования вообще, если ваш язык позволяет динамически наследовать от класса или даже экземпляра во время выполнения. Это вам совсем не нужно, если вы можете контролировать, какие методы подвергаются (и как они отображаются) в то же время.

Мое заключение: Делегирование является обходным путем для ошибки на языке программирования.

Ответ 5

Каждый знает, что полиморфизм - большое преимущество наследования. Другое преимущество, которое я нахожу в наследовании, - это то, что помогает создавать реплику реального мира. Например, в системе pay roll мы имеем дело с менеджерами-разработчиками, офисными мальчиками и т.д., Если мы наследуем весь этот класс с Super Class Employee. это делает нашу программу более понятной в контексте реального мира, что все эти классы в основном являются сотрудниками. И еще одно, что классы не только содержат методы, они также содержат атрибуты. Поэтому, если мы будем содержать атрибуты, общие для сотрудника в Employee класс, такой как возраст социального обеспечения и т.д., он обеспечил бы большее повторное использование кода и концептуальную ясность и, конечно же, полиморфизм. Однако при использовании вещей наследования мы должны иметь в виду основной принцип дизайна "Определите аспекты вашего приложения, которые меняются и отделяют их от тех аспектов, которые меняются". Вы никогда не должны реализовывать те аспекты приложения, которые вместо этого наследуют изменение наследования. И для тех аспектов, которые не являются изменчивыми, вы должны использовать наследование, если существует очевидное отношение "есть".

Ответ 6

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

Ответ 7

Интерфейсы определяют только то, что может делать объект, а не как. Таким образом, в простых терминах интерфейсы - это просто контракты. Все объекты, реализующие интерфейс, должны будут определить свою собственную реализацию контракта. В практическом мире это дает вам separation of concern. Представьте себе, что вы пишете приложение, которое должно иметь дело с различными объектами, о которых вы заранее не знаете, но вам все же нужно иметь дело с ними, только вы знаете, что все те вещи, которые должны выполнять эти объекты. Таким образом, вы определите интерфейс и укажите все операции в контракте. Теперь вы напишете свое приложение против этого интерфейса. Позже тот, кто хочет использовать ваш код или приложение, должен будет реализовать интерфейс на объекте, чтобы он работал с вашей системой. Ваш интерфейс заставит их объект определять, как должна выполняться каждая операция, определенная в контракте. Таким образом, каждый может писать объекты, которые реализуют ваш интерфейс, чтобы они безупречно адаптировались к вашей системе, и все, что вы знаете, это то, что нужно сделать, и именно этому объекту необходимо определить, как это делается.

В реальном мире это практика обычно известна как Programming to Interface and not to Implementation.

Интерфейсы - это просто контракты или подписи, и они не знают ничего о реализациях.

Кодирование от средств интерфейса, клиентский код всегда содержит объект Interface, который предоставляется factory. Любой экземпляр, возвращаемый factory, будет иметь тип Interface, который должен был реализовать любой класс кандидата factory. Таким образом, клиентская программа не беспокоится о реализации, и сигнатура интерфейса определяет, что все операции могут быть выполнены. Это можно использовать для изменения поведения программы во время выполнения. Это также помогает вам писать гораздо лучшие программы с точки зрения обслуживания.

Вот вам основной пример.

public enum Language
{
    English, German, Spanish
}

public class SpeakerFactory
{
    public static ISpeaker CreateSpeaker(Language language)
    {
        switch (language)
        {
            case Language.English:
                return new EnglishSpeaker();
            case Language.German:
                return new GermanSpeaker();
            case Language.Spanish:
                return new SpanishSpeaker();
            default:
                throw new ApplicationException("No speaker can speak such language");
        }
    }
}

[STAThread]
static void Main()
{
    //This is your client code.
    ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English);
    speaker.Speak();
    Console.ReadLine();
}

public interface ISpeaker
{
    void Speak();
}

public class EnglishSpeaker : ISpeaker
{
    public EnglishSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak English.");
    }

    #endregion
}

public class GermanSpeaker : ISpeaker
{
    public GermanSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak German.");
    }

    #endregion
}

public class SpanishSpeaker : ISpeaker
{
    public SpanishSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak Spanish.");
    }

    #endregion
}

alt text http://ruchitsurati.net/myfiles/interface.png

Ответ 8

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

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

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

Ответ 9

Вы писали:

[1] Многие люди используют классические наследование для достижения полиморфизма вместо того, чтобы сдавать свои классы реализовать интерфейс. Цель Наследование - это повторное использование кода, а не полиморфизм. Кроме того, некоторые люди использовать наследование для моделирования их интуитивное понимание "is-a" отношения, которые часто могут быть проблематичным.

В большинстве языков линия между "реализацией интерфейса" и "получением класса от другого" очень тонкая. Фактически, в таких языках, как С++, если вы выписываете класс B из класса A, а A - это класс, который состоит только из чистых виртуальных методов, вы реализуете интерфейс.

Наследование - это повторное использование интерфейса, а не повторное использование. Это не о повторном использовании кода, как вы писали выше.

Наследование, как вы правильно указываете, предназначено для моделирования отношения IS-A (тот факт, что многие люди ошибаются, не имеет ничего общего с наследованием). Вы также можете сказать "ПОВЕДЕНИЯ-НРАВИТСЯ-А". Однако только потому, что что-то имеет отношение IS-A к чему-то другому, не означает, что он использует тот же (или даже похожий) код для выполнения этих отношений.

Сравните этот пример С++, который реализует различные способы вывода данных; два класса используют (общедоступное) наследование, чтобы они могли получить доступ полиморфно:

struct Output {
  virtual bool readyToWrite() const = 0;
  virtual void write(const char *data, size_t len) = 0;
};

struct NetworkOutput : public Output {
  NetworkOutput(const char *host, unsigned short port);

  bool readyToWrite();
  void write(const char *data, size_t len);
};

struct FileOutput : public Output {
  FileOutput(const char *fileName);

  bool readyToWrite();
  void write(const char *data, size_t len);
};

Теперь представьте, что это была Java. "Выход" не был структурой, а "интерфейсом". Его можно назвать "Записываемым". Вместо "публичного вывода" вы бы сказали "реализует Writable". Какая разница в отношении дизайна?

Отсутствует.

Ответ 10

Когда вы спросите:

Даже если вы знаете, что классическое наследование не является неправильным для реализации определенной модели, является ли эта причина достаточной для использования вместо композиции?

Ответ - нет. Если модель неверна (с использованием наследования), чем неправильно использовать что бы то ни было.

Вот некоторые проблемы с наследованием, которые я видел:

  • Всегда нужно проверять тип времени выполнения указателей на производные классы, чтобы увидеть, могут ли они быть сброшены (или вниз тоже).
  • Это "тестирование" может быть достигнуто различными способами. У вас может быть какой-то виртуальный метод, который возвращает идентификатор класса. Или если вам не удастся реализовать RTTI (идентификация типа времени выполнения) (по крайней мере, в c/С++), который может дать вам удар производительности.
  • Типы классов, которые не могут получить "бросок", могут быть потенциально проблематичными.
  • Существует множество способов применить тип вашего класса вверх и вниз по дереву наследования.

Ответ 11

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

Есть действительно 3 способа справиться с этим:

  • Наследование.
  • Дублировать код (запах кода "Дублированный код" ).
  • Переместите логику еще на один класс (код "Lazy Class", "Средний человек", "Цепочки сообщений" и/или "Несовершенная близость" ).

Теперь может быть неправильное использование наследования. Например, Java имеет классы InputStream и OutputStream. Подклассы из них используются для чтения/записи файлов, сокетов, массивов, строк, а некоторые используются для переноса других потоков ввода/вывода. Основываясь на том, что они делают, это должны быть интерфейсы, а не классы.

Ответ 12

Один из самых полезных способов использования наследования - это объекты GUI.

Ответ 13

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

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