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

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

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

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

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

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

Обновить

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

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

Обновление 2

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

13 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

Мой вывод: делегирование - это обход ошибки в языке программирования.

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

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

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

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

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

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

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
}

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

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

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

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

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

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

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

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

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

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

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

Вы написали:

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

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

Наследование касается повторного использования интерфейса, а не повторного использования реализации. Речь идет не о повторном использовании кода, как вы написали выше.

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

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

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. "Вывод" был не структурой, а "интерфейсом". Это можно назвать "записываемым". Вместо "публичного вывода" вы бы сказали "реализует запись". Какая разница в дизайне?

Никто.

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

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

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

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

  1. Всегда нужно проверять тип времени выполнения указателей на производные классы, чтобы увидеть, могут ли они быть приведены (или тоже).
  2. Это "тестирование" может быть достигнуто различными способами. У вас может быть какой-то виртуальный метод, который возвращает идентификатор класса. Или если вам не удастся реализовать RTTI (идентификация типа времени выполнения) (по крайней мере, в c/ C++), что может привести к снижению производительности.
  3. типы классов, которые не могут быть "разобраны", могут быть потенциально проблематичными.
  4. Есть много способов привести тип вашего класса вверх и вниз по дереву наследования.
Другие вопросы по тегам