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

Это довольно спорная тема, и, прежде чем сказать «нет», действительно ли она действительно нужна?

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

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

Мое единственное предостережение: мы все равно разрешаем наследование интерфейсов.

(Update)

Давайте приведем пример того, почему это необходимо, вместо того, чтобы сказать: «иногда это просто необходимо». Это действительно бесполезно. Где ваше доказательство?

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

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

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();
    }
}

Ответы [ 20 ]

20 голосов
/ 12 декабря 2008

Я писал об этом как о странной идее некоторое время назад.

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

Это потенциальные языковые функции, такие как дополнения, которые облегчат жизнь, IMO.

10 голосов
/ 12 декабря 2008

Наследование может быть весьма полезным в ситуациях, когда в вашем базовом классе есть несколько методов с одинаковой реализацией для каждого производного класса, чтобы избавить каждый отдельный производный класс от необходимости реализовывать код стандартной платформы. Например, класс .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);
    }
}

}

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

6 голосов
/ 12 декабря 2008

Хотелось бы, чтобы языки предоставили некоторые механизмы, упрощающие делегирование переменным-членам. Например, предположим, что интерфейс I имеет 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 сложнее увидеть. Это также означает, что всякий раз, когда новые методы добавляются в интерфейс I, я должен явно добавлять больше кода в C1, чтобы делегировать эти новые методы в C1.

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

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.
    }
}

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

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

6 голосов
/ 12 декабря 2008

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

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

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

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

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

5 голосов
/ 12 декабря 2008

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

Давайте возьмем мой мир на данный момент, который в основном разрабатывается на C #.

Чтобы 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 #, новый язык, такой как C #, но без наследования на основе классов. Другими словами, даже если вы планируете создать совершенно новый язык, я все еще не совсем уверен, какой будет конечная цель.

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

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

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

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

5 голосов
/ 08 января 2009

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

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

Если вам не нравится наследование, я предлагаю прекратить использование .NET все вместе. Существует слишком много сценариев, в которых это экономит время (см. СУХОЙ: не повторяйтесь).

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

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

4 голосов
/ 12 декабря 2008

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

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

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

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

3 голосов
/ 12 декабря 2008

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

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

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

Вопросы типа «Когда мы должны использовать наследование» или «Когда мы не должны» намного более полезны.

3 голосов
/ 12 декабря 2008

Полагаю, я должен сыграть адвоката дьявола. Если бы у нас не было наследования, мы бы не смогли наследовать абстрактные классы, использующие шаблонный шаблон. Есть много примеров, когда это используется в таких средах, как .NET и Java. Поток в 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();

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

// 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();

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

2 голосов
/ 12 декабря 2008

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

...