Когда использовать интерфейсы или абстрактные классы? Когда использовать оба?

В некоторых рекомендациях указано, что вы должны использовать интерфейс, если хотите определить контракт для класса, где наследование не ясно (IDomesticated) и наследование, когда класс является расширением другого (Cat : Mammal, Snake : Reptile), бывают случаи, когда (на мой взгляд) эти рекомендации входят в серое пространство.

Например, скажем, моя реализация была Cat : Pet. Pet - абстрактный класс. Должно ли быть расширено до Cat : Mammal, IDomesticated, где Mammal является абстрактным классом, а IDomesticated является интерфейсом? Или я в конфликте с KISS/YAGNI (хотя я не уверен, будет ли в будущем класс Wolf, который не сможет наследовать от Pet)?

Переходя от метафорических Cat и Pet s, скажем, у меня есть некоторые классы, которые представляют источники для входящих данных. Все они должны каким-то образом реализовать ту же базу. Я мог бы реализовать некоторый общий код в абстрактном классе Source и наследовать его. Я мог бы просто создать интерфейс ISource (который мне кажется более "правильным" ) и повторно реализовать общий код в каждом классе (который менее интуитивно понятен). Наконец, я мог "взять пирог и съесть его", сделав как абстрактный класс, так и интерфейс. Что лучше?

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


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

Также стоит отметить, что этот вопрос в основном относится к языкам, которые не поддерживают множественное наследование (например,.NET и Java).

Ответ 1

Как первое правило, я предпочитаю абстрактные классы по интерфейсам, на основе .NET Design Guidelines. Обоснование применяется намного шире, чем .NET, но лучше объясняется в книге Руководство по разработке каркаса.

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

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

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

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

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

Примером, который приходит на ум, является ASP.NET MVC. Конвейер запросов работает на IController, но есть базовый класс контроллера, который обычно используется для реализации поведения.

Окончательный ответ: если использовать абстрактный базовый класс, используйте только это. Если вы используете интерфейс, базовый класс является дополнительной вежливостью для разработчиков.


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

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

Ответ 2

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

Кошка является млекопитающим, но одна из ее возможностей заключается в том, что она является Pettable.

Или, говоря иначе, классы - существительные, а интерфейсы - ближе к прилагательным.

Ответ 3

Из MSDN, Рекомендации для абстрактных классов и интерфейсов

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

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

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

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

Ответ 4

Я всегда использую эти рекомендации:

  • Использовать интерфейсы для множественного наследования TYPE (поскольку .NET/Java не использует множественное наследование)
  • Используйте абстрактные классы для повторно используемой реализации типа

Правило доминирующей озабоченности диктует, что у класса всегда есть основная проблема и 0 или более других (см. http://citeseer.ist.psu.edu/tarr99degrees.html). Эти 0 или более других, которые вы затем реализуете через интерфейсы, поскольку класс затем реализует все типы, которые он должен реализовать (собственные и все интерфейсы, которые он реализует).

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

Ответ 5

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

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

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

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

interface Foo
abstract class FooBase implements Foo
class FunnyFoo extends FooBase
class SeriousFoo extends FooBase

У вас также может быть больше абстрактных классов, наследующих друг от друга для более сложной иерархии.

Ответ 6

Существует также нечто, называемое DRY - не повторяйте себя.

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

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

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

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

Ответ 7

См. ниже вопрос SE для общих рекомендаций:

Интерфейс против абстрактного класса (общий OO)

Практический пример использования интерфейса:

  • Реализация Strategy_pattern: Определите свою стратегию как интерфейс. Динамически переключайте реализацию с помощью одной из конкретных реализаций стратегии во время выполнения.

  • Определите возможность среди нескольких несвязанных классов.

Практический пример использования для абстрактного класса:

  • Реализация Template_method_pattern: Определить скелет алгоритма. Детские классы не могут изменять структуру альгортимов, но они могут переопределить часть реализации в дочерних классах.

  • Если вы хотите использовать нестатические и не конечные переменные между несколькими связанными классами с отношением с.

Использование обоих классов и интерфейса abstradt:

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