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


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

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

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

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

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

обновление

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

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

обновление 2

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

13 57

13 ответов:

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

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

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

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

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

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

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

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

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

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

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

каждый знает, что полиморфизм является большим преимуществом наследования. Еще одно преимущество, которое я нахожу в наследстве, - это то, что помогает создавать реплики реального мира. Например, в системе pay roll мы имеем дело с разработчиками менеджеров, офисными мальчиками и т. д. если мы наследуем все эти классы с супер классом сотрудника. это делает нашу программу более понятной в контексте реального мира, что все эти классы являются в основном сотрудники. И еще одна вещь классы не только содержат методы они также содержат атрибуты. Поэтому, если мы содержим атрибуты, общие для сотрудника в сотруднике класс, как номер социального страхования, возраст и т. д. это обеспечит большее повторное использование кода и концептуальную ясность и, конечно же, полиморфизм. Однако при использовании наследования вещей мы должны иметь в виду основной принцип проектирования "определить аспекты вашего приложения, которые изменяются и отделить их от тех аспектов, которые изменяются". Вы никогда не должны реализовывать те аспекты приложения, которые изменяются по наследованию вместо использования композиции. И для тех аспектов, которые не изменчивы, 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. настраиваемые политики должны знать о базовом классе, могут использоваться только с базовым классом и не имеют смысла в любом другом контексте. Использование стратегии вместо этого возможно, но Пита, потому что как базовый класс, так и политика класс должен иметь ссылки друг на друга.

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

вы писали:

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

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

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

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

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

нет.

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

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

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

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

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

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

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

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

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

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

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