Являются ли абстрактные классы / методы устаревшими?

37

Я использовал для создания много абстрактных классов / методов. Затем я начал использовать интерфейсы.

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

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

Итак, являются ли абстрактные классы / методы устаревшими?

Борис Янков
источник
Как насчет того, если выбранный вами язык программирования не поддерживает интерфейсы? Кажется, я помню, что это так для C ++.
Бернард
7
@Bernard: в C ++ абстрактный класс - это интерфейс во всем, кроме имени. То, что они также могут делать больше, чем «чистые» интерфейсы, не является недостатком.
gbjbaanb
@gbjbaanb: я полагаю. Я не припоминаю их использование в качестве интерфейсов, а скорее для обеспечения реализации по умолчанию.
Бернард
Интерфейсы являются «валютой» ссылок на объекты. Вообще говоря, они являются основой полиморфного поведения. Абстрактные классы служат другой цели, которую Deadalnix объяснил прекрасно.
Джигги
4
Разве это не похоже на высказывание: «Устаревшие виды транспорта теперь у нас есть автомобили?» Да, большую часть времени вы пользуетесь машиной. Но если вам когда-нибудь понадобится что-то кроме автомобиля или нет, было бы неправильно сказать «мне не нужно использовать виды транспорта». Интерфейс является таким же , как абстрактным класс без какой - либо реализации, а также со специальным именем, нет?
Джек В.

Ответы:

111

Нет.

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

Это также действительно хороший способ уменьшить последовательную связь. Без абстрактного метода / классов вы не можете реализовать шаблон шаблона шаблона. Я предлагаю вам взглянуть на эту статью в Википедии: http://en.wikipedia.org/wiki/Template_method_pattern

deadalnix
источник
3
@deadalnix Шаблон метода шаблона может быть довольно опасным. Вы можете легко получить высокосвязанный код и начать взламывать код, чтобы расширить шаблонный абстрактный класс для обработки «еще одного случая».
quant_dev
9
Это больше похоже на анти-паттерн. Вы можете получить то же самое с композицией в интерфейсе. То же самое относится и к частичным классам с некоторыми абстрактными методами. Вместо того , заставляя код клиента подкласс и переопределить их, вы должны иметь осуществление быть введены . Смотрите: en.wikipedia.org/wiki/Composition_over_inheritance#Benefits
back2dos
4
Это совсем не объясняет, как уменьшить последовательную связь. Я согласен с тем, что существует несколько способов достижения этой цели, но, пожалуйста, ответьте на проблему, о которой вы говорите, вместо того, чтобы слепо ссылаться на какой-то принцип. Вы закончите программированием культа грузов, если не сможете объяснить, почему это связано с реальной проблемой.
Deadalnix
3
@deadalnix: Сказать, что последовательное соединение лучше всего уменьшить с помощью шаблона, - это программирование культа груза (использование шаблона состояния / стратегии / фабрики также может сработать), а также предположение о том, что оно всегда должно быть уменьшено. Последовательная связь часто является простым следствием предоставления очень тонкого контроля над чем-либо, что означает только компромисс. Это по-прежнему не означает, что я не могу написать оболочку, у которой нет последовательной связи с использованием композиции. На самом деле, это делает это намного проще.
back2dos
4
У Java теперь есть реализации по умолчанию для интерфейсов. Это меняет ответ?
raptortech97
15

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

public void DoTask()
{
    doSetup();
    DoWork();
    doCleanup();
}

protected abstract void DoWork();

Это достаточно хороший способ реализовать отверстие в среднем шаблоне без знания производного класса о действиях по настройке и очистке. Без абстрактных методов вам придется полагаться на производный класс, реализующий DoTaskи запоминающий вызов base.DoSetup()и base.DoCleanup()все время.

редактировать

Кроме того, спасибо Deadalnix за публикацию ссылки на шаблонный шаблон , который я описал выше, фактически не зная имени. :)

Скотт Уитлок
источник
2
+1, я предпочитаю твой ответ deadalnix '. Обратите внимание, что вы можете реализовать шаблонный метод более простым способом, используя делегаты (в C #):public void DoTask(Action doWork)
Joh
1
@Joh - правда, но это не обязательно так хорошо, в зависимости от вашего API. Если ваш базовый класс Fruitи ваш производный класс Apple, вы хотите позвонить myApple.Eat(), а не myApple.Eat((a) => howToEatApple(a)). Кроме того, вы не хотите Appleзвонить base.Eat(() => this.howToEatMe()). Я думаю, что чище просто переопределить абстрактный метод.
Скотт Уитлок
1
@ Скотт Уитлок: Ваш пример использования яблок и фруктов слишком отстранен от реальности, чтобы судить о том, является ли получение наследства предпочтительным для делегатов в целом. Очевидно, что ответ «это зависит», так что это оставляет много места для дискуссий ... В любом случае, я нахожу, что методы шаблона часто происходят во время рефакторинга, например, при удалении дублирующегося кода. В таких случаях я обычно не хочу связываться с иерархией типов и предпочитаю избегать наследования. Этот вид хирургии легче делать с лямбдами.
Джох
Этот вопрос иллюстрирует больше, чем простое применение шаблона Template Template - публичный / непубличный аспект известен в сообществе C ++ как шаблон Non Virtual Interface . Имея открытую не виртуальную функцию, оберните виртуальную - даже если изначально первая не делает ничего, кроме вызова второй - вы оставляете точку настройки для настройки / очистки, регистрации, профилирования, проверок безопасности и т. Д., Которые могут понадобиться позже.
Тони
10

Нет, они не устарели.

На самом деле, между абстрактными классами / методами и интерфейсами есть неясное, но принципиальное отличие.

если набор классов, в которых должен использоваться один из них, имеет общее поведение, которое они разделяют (я имею в виду связанные классы), то переходите к абстрактным классам / методам.

Пример: clerk, Officer, Director - все эти классы имеют общую CalculateSalary (), используют абстрактные базовые классы. CalculateSalary () может быть реализован по-разному, но есть некоторые другие вещи, например, GetAttendance (), которые имеют общее определение в базовом классе.

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

Пример: корова, скамейка, машина, классы, не связанные с телепопой, но Isortable может быть там для сортировки их в массив.

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

WinW
источник
6

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

public abstract class Frobber
{
    private Frobber() {}
    public abstract void Frob(Frotz frotz);
    private class GreenFrobber : Frobber
    { ... }
    private class RedFrobber : Frobber
    { ... }
    public static Frobber GetFrobber(bool b) { ... } // return a green or red frobber
}

public sealed class Frotz
{
    public void Frobbit(Frobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

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

Если вместо этого мы говорим:

public interface IFrobber
{
    void Frob(Frotz frotz);
}
public class GreenFrobber : IFrobber
{ ... }
public class RedFrobber : Frobber
{ ... }

public sealed class Frotz
{
    public void Frobbit(IFrobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Теперь я абсолютно ничего не знаю о последствиях этого звонка Фробу. Мне нужно быть уверенным, что весь код на Frobbit устойчив к любой возможной реализации IFrobber , даже реализациям людей, которые некомпетентны (плохи) или активно враждебны ко мне или моим пользователям (намного хуже).

Абстрактные классы позволяют избежать всех этих проблем; используй их!

Эрик Липперт
источник
1
Проблема, о которой вы говорите, должна быть решена с помощью алгебраических типов данных, а не сгибания классов до точки, где они нарушают принцип открытия / закрытия.
back2dos
1
Во-первых, я никогда не говорил, что это хорошо или морально . Вы положили это в мой рот. Во-вторых (это моя актуальная точка зрения): это всего лишь плохое оправдание отсутствующей языковой функции. Наконец, есть решение без абстрактных классов: pastebin.com/DxEh8Qfz . В противоположность этому, в вашем подходе используются вложенные и абстрактные классы, которые объединяют все, кроме кода, требующего безопасности, в большой шарик грязи. Нет веской причины, по которой RedFrobber или GreenFrobber должны быть привязаны к ограничениям, которые вы хотите применить. Это увеличивает сцепление и блокирует во многих решениях без какой-либо выгоды.
back2dos
1
Безусловно, неверно утверждать, что абстрактные методы устарели, но, с другой стороны, утверждать, что они должны быть предпочтительнее интерфейсов, вводит в заблуждение. Они просто инструмент для решения проблем, отличных от интерфейсов.
Гроо
2
«есть принципиальное отличие [...] интерфейсов гораздо менее надежных [...], чем абстрактные классы». Я не согласен, различие, которое вы проиллюстрировали в своем коде, основано на ограничениях доступа, которые, я думаю, не имеют оснований существенно различаться между интерфейсами и абстрактными классами. Это может быть так в C #, но вопрос не зависит от языка.
Джох
1
@ back2dos: Я просто хотел бы отметить, что решение Эрика фактически использует алгебраический тип данных: абстрактный класс является типом суммы. Вызов абстрактного метода аналогичен сопоставлению с образцом для набора вариантов.
Родрик Чепмен,
4

Вы сами говорите:

Вам нужен абстрактный класс с некоторой реализацией в нем? Создайте интерфейс, создайте класс. Наследуй класс, реализуй интерфейс

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

gbjbaanb
источник
Не говоря уже о том, что если класс не наследует интерфейс (что не было упомянуто, он должен), вы должны написать функции пересылки на некоторых языках.
Sjoerd
Хм, дополнительная «большая работа», которую вы упомянули, заключается в том, что вы создали интерфейс и реализовали его в классах - это действительно кажется большой работой? Кроме того, конечно, интерфейс дает вам возможность реализовать интерфейс из новой иерархии, которая не будет работать с абстрактным базовым классом.
Билл К
4

Как я прокомментировал пост @deadnix: Частичные реализации являются анти-паттерном, несмотря на то, что шаблон шаблона их формализует.

Чистое решение для этого примера шаблона шаблона в Википедии :

interface Game {
    void initialize(int playersCount);
    void makePlay(int player);
    boolean done();
    void finished();
    void printWinner();
}
class GameRunner {
    public void playOneGame(int playersCount, Game game) {
        game.initialize(playersCount);
        int j = 0;
        for (int i = 0; !game.finished(); i++)
             game.makePlay(i % playersCount);
        game.printWinner();
    }
} 
class Monopoly implements Game {
     //... implementation
}

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

back2dos
источник
+1. В качестве примечания: «Частичные реализации являются антишаблоном, несмотря на то, что шаблон шаблона их формализует». Описание в википедии четко определяет шаблон, только пример кода является «неправильным» (в том смысле, что он использует наследование, когда он не нужен и существует более простая альтернатива, как показано выше). Другими словами, я не думаю, что виноват сам шаблон, только то, как люди стремятся его реализовать.
Джох
2

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

DeadMG
источник
-1. Я не понимаю, что "общий код против наследования" имеет отношение к вопросу. Вы должны проиллюстрировать или обосновать, почему «абстрактные классы имеют значительные преимущества перед интерфейсами».
Джох
1

Абстрактные классы не являются интерфейсами. Это классы, которые не могут быть созданы.

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

Но тогда у вас будет неабстрактный бесполезный класс. Абстрактные методы необходимы для заполнения функциональной дыры в базовом классе.

Например, учитывая этот класс

public abstract class Frobber {
    public abstract void Frob();

    public abstract boolean IsFrobbingNeeded { get; }

    public void FrobUntilFinished() {
        while (IsFrobbingNeeded) {
            Frob();
        }
    }
}

Как бы вы реализовали эту базовую функциональность в классе, который не имеет ни того, Frob()ни другого IsFrobbingNeeded?

конфигуратор
источник
1
Интерфейс также не может быть создан.
Sjoerd
@Sjoerd: Но вам нужен базовый класс с разделяемой реализацией; это не может быть интерфейсом.
конфигуратор
1

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

Дмитрий Р
источник
0

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

Основное использование абстрактного класса - отложить часть реализации; сказать, «эта часть реализации класса может быть сделано по-другому».

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

Дейв Кузино
источник
0

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

Тем не менее, если мы используем реальную аналогию, насколько полезны чистые интерфейсы, полностью лишенные функциональности и состояния? Если в качестве примера я использую USB, то это достаточно стабильный интерфейс (я думаю, что мы сейчас находимся на USB 3.2, но он также поддерживает обратную совместимость).

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

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

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

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

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


источник