Подтвердить что ты не робот

Следует ли удалить наследование (из не-интерфейсных типов) из языков программирования?

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

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

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

Мое единственное предостережение в этом заключается в том, что мы все же разрешаем наследование интерфейсов.

(Обновление)

Давайте дать пример того, зачем ему нужно, а не говорить: "Иногда это просто нужно". Это действительно не полезно вообще. Где ваше доказательство?

(Пример кода обновления 2)

Вот пример классической фигуры, более мощный и более явный ИМО, без наследования. В реальном мире почти никогда не бывает, что что-то действительно "является" чем-то другим. Почти всегда "Выполняется с точки зрения" более точно.

public interface IShape
{
    void Draw();
}

public class BasicShape : IShape
{
    public void Draw()
    {
        // All shapes in this system have a dot in the middle except squares.
        DrawDotInMiddle();
    }
}

public class Circle : IShape
{
    private BasicShape _basicShape;

    public void Draw()
    {
        // Draw the circle part
        DrawCircle();
        _basicShape.Draw();
    }
}

public class Square : IShape
{
    private BasicShape _basicShape;

    public void Draw()
    {
        // Draw the circle part
        DrawSquare();
    }
}
4b9b3361

Ответ 1

I писал об этом как дурацкую идею некоторое время назад.

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

Это потенциальные языковые функции, такие как микширование, которое облегчит жизнь без ИМО.

Ответ 2

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

public virtual int Read(byte[] buffer, int index, int count)
{
}

public int ReadByte()
{
    // note: this is only an approximation to the real implementation
    var buffer = new byte[1];
    if (this.Read(buffer, 0, 1) == 1)
    {
        return buffer[0];
    }

    return -1;
}

Поскольку наследование доступно, базовый класс может реализовать метод ReadByte для всех реализаций без необходимости беспокоиться об этом. В классе есть ряд других методов, которые имеют стандартные или фиксированные реализации. Таким образом, в этом типе ситуации это очень ценная вещь, по сравнению с интерфейсом, в котором ваши параметры должны либо сделать все повторное выполнение всего, либо создать класс типа StreamUtil, который они могут назвать (yuk!).

Чтобы прояснить, с наследованием все, что мне нужно написать для создания класса DerivedStream, это что-то вроде:

public class DerivedStream : Stream
{
    public override int Read(byte[] buffer, int index, int count)
    {
        // my read implementation
    }
}

Если мы используем интерфейсы и стандартную реализацию методов в StreamUtil, мне нужно написать еще больше кода:

public class DerivedStream : IStream
{
    public int Read(byte[] buffer, int index, int count)
    {
        // my read implementation
    }

    public int ReadByte()
    {
        return StreamUtil.ReadByte(this);
    }
}

}

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

Ответ 3

Конечно, вы можете хорошо писать отличные программы без объектов и наследования; функциональные программисты делают это все время. Но давайте не будем поспешными. Любой, кто интересуется этой темой, должен проверить слайды от Xavier Leroy, пригласил лекцию о классах против модулей в Objective Caml. Ксавье прекрасно справляется с тем, что наследование делает хорошо и не преуспевает в контексте различных видов эволюции программного обеспечения.

Все языки являются Turing-complete, поэтому, конечно, наследование не требуется. Но в качестве аргумента для значения наследования я представляю Smalltalk blue book, особенно иерархию Collection и Иерархия чисел. Я очень впечатлен тем, что квалифицированный специалист может добавить совершенно новый вид номера (или коллекции), не нарушая существующую систему.

Я также напомню вопрос о "приложении-убийце" для наследования: инструментарий GUI. Хорошо разработанный инструментарий (если вы его найдете) позволяет очень легко добавлять новые виды графических интерактивных виджетов.

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

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

Ответ 4

Я бы хотел, чтобы языки предоставили некоторые механизмы, чтобы упростить делегирование переменных-членов. Например, предположим, что интерфейс я имеет 10 методов, а класс C1 реализует этот интерфейс. Предположим, я хочу реализовать класс C2, который похож на C1, но с переопределенным методом m1(). Без использования наследования я бы сделал это следующим образом (на Java):

public class C2 implements I {
    private I c1;

    public C2() {
       c1 = new C1();
    }

    public void m1() {
        // This is the method C2 is overriding.
    }

    public void m2() {
        c1.m2();
    }

    public void m3() {
        c1.m3();
    }

    ...

    public void m10() {
        c1.m10();
    }
}

Другими словами, я должен явно писать код, чтобы делегировать поведение методов m2..m10 переменной-члену m1. Это немного боль. Он также загромождает код, так что сложнее увидеть реальную логику в классе C2. Это также означает, что всякий раз, когда к интерфейсу я добавляются новые методы, я должен явно добавить больше кода в C1, чтобы делегировать эти новые методы в C1.

Я хотел бы, чтобы языки позволяли мне говорить: C1 реализует I, но если C1 не хватает какого-либо метода из I, автоматически делегируем членскую переменную c1. Это сократило бы размер C1 до просто

public class C2 implements I(delegate to c1) {
    private I c1;

    public C2() {
       c1 = new C1();
    }

    public void m1() {
        // This is the method C2 is overriding.
    }
}

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

Здесь статья в блоге Я написал об автоматическом делегировании.

Ответ 5

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

Позвольте взять мой мир на данный момент, что в основном является развитием С#.

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

Другими словами, следующий код:

public interface IPerson { ... }
public interface IEmployee : IPerson { ... }
public class Employee : IEmployee
{
    private Person _Person;
    ...

    public String FirstName
    {
        get { return _Person.FirstName; }
        set { _Person.FirstName = value; }
    }
}

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

public class Employee : IEmployee
{
    private Person _Person implements IPerson;
    ...
}

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

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

Кроме того, вы должны удалить такие вещи, как видимость. У интерфейса действительно есть только два параметра видимости: Там и не там. В некоторых случаях вы бы или, как мне кажется, вынуждены были раскрывать больше своих внутренних данных, чтобы кто-то еще мог более легко использовать ваш класс.

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

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

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

В верхней части этого списка: ключевое слово с в Delphi. Это ключевое слово не просто стреляет в ногу, он, как компилятор, покупает дробовик, приходит к вам домой и нацеливается на вас.

Лично мне нравится наследование на основе классов. Конечно, вы можете написать себе в угол. Но мы все можем это сделать. Удалите классовое наследование, я просто найду новый способ стрелять в ногу.

Теперь, где я положил этот дробовик...

Ответ 6

Удачи, реализуя ISystemObject на всех ваших классах, чтобы у вас был доступ к ToString() и GetHashcode().

Кроме того, удачи в интерфейсе ISystemWebUIPage.

Если вам не нравится наследование, мое предложение состоит в том, чтобы прекратить использование .NET вместе. Есть слишком много сценариев, где это экономит время (см. СУХИЕ: не повторяйте себя).

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

Я предпочитаю интерфейсы, но они не являются серебряной пулей.

Ответ 7

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

Для меня это не только более читаемо, но и гораздо более проверяемо, а также упрощает рефакторинг.

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

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

Ответ 8

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

// Alternative 1:
public class MyThread extends Thread {

    // Abstract method to implement from Thread
    // aka. "template method" (GoF design pattern)
    public void run() {
        // ...
    }
}

// Usage:
MyThread t = new MyThread();
t.start();

Альтернатива, в моем смысле, многословная, когда вы должны ее использовать. Сложность визуального clutteer возрастает. Это связано с тем, что вам необходимо создать Thread, прежде чем вы сможете его использовать.

// Alternative 2:
public class MyThread implements Runnable {
    // Method to implement from Runnable:
    public void run() {
        // ...
    }
}

// Usage:
MyThread m = new MyThread();
Thread t = new Thread(m);
t.start();
// …or if you have a curious perversion towards one-liners
Thread t = new Thread(new MyThread());
t.start();

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

Ответ 9

нужно? Нет. Вы можете написать любую программу на C, например, которая не имеет никакого наследования или объектов. Вы можете написать его на ассемблере, хотя он будет менее переносимым. Вы могли бы написать его на машине Тьюринга и эмулировать ее. Кто-то разработал компьютерный язык с одной инструкцией (что-то вроде вычитания и ветвления, если не ноль), и вы могли бы написать свою программу в этом.

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

Поэтому я нахожу такие вопросы бесполезными.

Вопросы, подобные "Когда следует использовать наследование" или "Когда не следует", намного полезнее.

Ответ 10

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

Ответ 11

Нет. Просто потому, что это не часто требуется, это не значит, что он никогда не нужен. Как и любой другой инструмент в наборе инструментов, он может (и был, и будет) использоваться неправильно. Однако это не означает, что он должен никогда использоваться. На самом деле, на некоторых языках (С++) не существует такой вещи, как "интерфейс" на уровне языка, поэтому без значительных изменений вы не можете запретить его.

Ответ 12

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

Ответ 13

Моя первая мысль была: Ты сумасшедший. Но, подумав об этом, я с тобой согласен. Я не говорю, что удалить наследование класса полностью (абстрактные классы с частичной реализацией, к примеру, могут быть полезны), но я часто унаследовал (каламбур) плохо написанный OO-код с наследованием многоуровневого класса, который ничего не добавил, кроме наворотов, к код.

Ответ 14

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

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

Ответ 15

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

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

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

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

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

Ответ 17

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

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

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

Некоторые из его пунктов:

это помогает при тестировании и улучшает читаемость.

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

Тем не менее, для глубоких иерархий наследования. Вы в идеале хотите смотреть только в одном месте.

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

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

также упрощает рефакторинг.

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

Так что злоупотребление наследованием - это плохо. Чрезмерное использование интерфейсов - это плохо. И в идеале, класс не наследует ни от чего (кроме, может быть, "объекта" или эквивалента языка), ни для реализации каких-либо интерфейсов. Но это не означает, что любая функция должна быть удалена с языка.

Ответ 18

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

Аналогично, что касается Java, например, где каждый объект наследуется от Object, и поэтому автоматически имеет реализации equals, hashcode и т.д. Мне не нужно их повторно выполнять, и он "просто работает", когда я хотите использовать объект в качестве ключа в хэш-таблице. Мне не нужно писать прокси-версию по умолчанию в метод Hashtable.hashcode(Object o), который, откровенно говоря, кажется, что он перемещает в сторону из ориентации объекта.

Ответ 19

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

Есть некоторые исследования, которые вводят черты в С# (pdf).

Perl имеет черты через Moose:: Роли. Scala черты похожи на mixins, как в Ruby.

Ответ 20

Вопрос: "Следует ли удалить наследование (из типов без интерфейса) из языков программирования?"

Я говорю "Нет", так как это сломает много существующего кода.

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

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

Сказав это, парадигма mixin довольно уникальна для С++. Без этого я ожидаю, что наследование очень привлекательно для прагматичного программиста.