Зачем вообще использовать наследование? [закрытый]

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

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

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

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

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

обновление

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

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

обновление 2

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

13 ответов


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

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


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

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

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

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

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


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


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

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

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


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


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


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

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

интерфейсы контракты или просто подписи и они не знают что-нибудь о реализациях.

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

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

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-текст http://ruchitsurati.net/myfiles/interface.png


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

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

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


вы писали:

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

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

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

наследование, как вы правильно указываете, предназначено для моделирования отношения 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);
};

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

нет.


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

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

  1. наследование.
  2. дублировать код (код запах "дублирование кода").
  3. переместите логику в еще один класс (код пахнет "ленивым классом", "средним человеком", "цепочками сообщений" и / или " неуместным Близость.)"

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


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


когда ты спросил:

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

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

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

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

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

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