Когда использовать интерфейсы или абстрактные классы? Когда использовать оба?
В то время как в некоторых правилах говорится, что вы должны использовать интерфейс, когда вы хотите определить контракт для класса, где наследование неясно (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).
7 ответов
В качестве первого практического правила я предпочитаю абстрактные классы, а не интерфейсы, основанные на.NET Design Guidelines. Рассуждения применяются гораздо шире, чем.NET, но лучше объяснены в книге Framework Design Guidelines.
Основным аргументом в пользу предпочтения абстрактных базовых классов является управление версиями, потому что вы всегда можете добавить нового виртуального члена в абстрактный базовый класс, не нарушая существующих клиентов. Это невозможно с интерфейсами.
Существуют сценарии, в которых интерфейс по-прежнему является правильным выбором (особенно, если вам не нужны версии), но знание о преимуществах и недостатках позволяет вам принять правильное решение.
Итак, в качестве частичного ответа, прежде чем я продолжу: наличие интерфейса и базового класса имеет смысл только в том случае, если вы сначала решите кодировать интерфейс. Если вы разрешаете интерфейс, вы должны кодировать только этот интерфейс, так как в противном случае вы нарушили бы принцип подстановки Лискова. Другими словами, даже если вы предоставляете базовый класс, который реализует интерфейс, вы не можете позволить своему коду использовать этот базовый класс.
Если вы решите кодировать против базового класса, иметь интерфейс не имеет смысла.
Если вы решите кодировать интерфейс, иметь базовый класс, который обеспечивает функциональность по умолчанию, необязательно. Это не обязательно, но может ускорить процесс реализации, поэтому вы можете предоставить его в качестве любезности.
На ум приходит пример из ASP.NET MVC. Конвейер запросов работает на IController, но есть базовый класс Controller, который вы обычно используете для реализации поведения.
Окончательный ответ: если вы используете абстрактный базовый класс, используйте только это. Если используется интерфейс, базовый класс является необязательной любезностью для разработчиков.
Обновление: я больше не предпочитаю абстрактные классы по интерфейсам, и я не делаю это долгое время; вместо этого я предпочитаю композицию наследованию, используя SOLID в качестве руководства.
(Хотя я мог бы редактировать вышеупомянутый текст напрямую, это радикально изменило бы характер поста, и, поскольку несколько человек сочли его достаточно ценным, чтобы проголосовать за него, я бы предпочел оставить исходный текст стоять и вместо этого добавить этот примечание. Последняя часть поста все еще имеет смысл, поэтому было бы стыдно и ее удалить.)
Я склонен использовать базовые классы (абстрактные или нет), чтобы описать что-то, в то время как я использую интерфейсы для описания возможностей объекта.
Кошка - это млекопитающее, но одна из его возможностей заключается в том, что он - Pettable.
Или, другими словами, классы являются существительными, а интерфейсы отображаются ближе к прилагательным.
От MSDN, Рекомендации для абстрактных классов и интерфейсов
Если вы планируете создать несколько версий вашего компонента, создайте абстрактный класс. Абстрактные классы предоставляют простой и легкий способ создания версий ваших компонентов. При обновлении базового класса все наследующие классы автоматически обновляются с изменением. Интерфейсы, с другой стороны, не могут быть изменены после создания. Если требуется новая версия интерфейса, вы должны создать совершенно новый интерфейс.
Если создаваемая вами функциональность будет полезна для широкого круга разнородных объектов, используйте интерфейс. Абстрактные классы следует использовать в первую очередь для объектов, которые тесно связаны, тогда как интерфейсы лучше всего подходят для предоставления общих функций несвязанным классам.
Если вы разрабатываете небольшие, лаконичные кусочки функциональности, используйте интерфейсы. Если вы разрабатываете большие функциональные блоки, используйте абстрактный класс.
Если вы хотите обеспечить общую, реализованную функциональность среди всех реализаций вашего компонента, используйте абстрактный класс. Абстрактные классы позволяют вам частично реализовать ваш класс, тогда как интерфейсы не содержат реализации для каких-либо членов.
Я всегда использую эти рекомендации:
- Используйте интерфейсы для множественного наследования TYPE (поскольку.NET/Java не использует множественное наследование)
- Используйте абстрактные классы для многократного использования реализации типа
Правило доминирующего беспокойства гласит, что класс всегда имеет основное беспокойство и 0 или более других (см. http://citeseer.ist.psu.edu/tarr99degrees.html). Те 0 или более других вы затем реализуете через интерфейсы, поскольку класс затем реализует все типы, которые он должен реализовать (свой собственный, и все интерфейсы, которые он реализует).
В мире множественного наследования реализации (например, C++/Eiffel) можно было бы наследовать от классов, которые реализуют интерфейсы. (Теоретически. На практике это может не сработать.)
Если вы хотите предоставить возможность полной замены вашей реализации, используйте интерфейс. Это особенно относится к взаимодействиям между основными компонентами, они всегда должны быть отделены интерфейсами.
Также могут быть технические причины для предпочтения интерфейса, например, для включения моделирования в модульных тестах.
Внутри компонента может быть достаточно просто использовать абстрактный класс напрямую для доступа к иерархии классов.
Если вы используете интерфейс и имеете иерархию реализации классов, то хорошей практикой будет иметь абстрактный класс, содержащий общие части реализации. Например
interface Foo
abstract class FooBase implements Foo
class FunnyFoo extends FooBase
class SeriousFoo extends FooBase
Вы также можете иметь более абстрактные классы, наследуемые друг от друга для более сложной иерархии.
См. Ниже вопрос SE для общих руководящих принципов:
Интерфейс против абстрактного класса (общий ОО)
Практический пример использования интерфейса:
Реализация Strategy_pattern: определите свою стратегию как интерфейс. Динамическое переключение реализации с одной из конкретных реализаций стратегии во время выполнения.
Определить возможность среди нескольких не связанных классов.
Практический пример использования абстрактного класса:
Реализация https://en.wikipedia.org/wiki/Template_method_pattern: определение каркаса алгоритма. Дочерние классы не могут изменить структуру алгоритма, но они могут переопределить часть реализации в дочерних классах.
Когда вы хотите разделить нестатические и не финальные переменные между несколькими связанными классами с отношением "имеет".
Использование класса и интерфейса abstradt:
Если вы собираетесь использовать абстрактный класс, вы можете переместить абстрактные методы в интерфейс, а абстрактный класс может просто реализовать этот интерфейс. Все случаи использования абстрактных классов могут попадать в эту категорию.
Есть также то, что называется принципом СУХОЙ - не повторяй себя.
В вашем примере источников данных вы говорите, что есть некоторый общий код, который является общим для разных реализаций. Мне кажется, что лучший способ справиться с этим - это иметь абстрактный класс с общим кодом и некоторыми конкретными классами, расширяющими его.
Преимущество состоит в том, что каждое исправление ошибки в общем коде приносит пользу всем конкретным реализациям.
Если вы переходите только на интерфейс, вам придется поддерживать несколько копий одного и того же кода, что вызывает проблемы.
Что касается абстрактного интерфейса +, если для этого нет немедленного оправдания, я бы этого не делал. Извлечение интерфейса из абстрактного класса - это простой рефакторинг, поэтому я буду делать это только тогда, когда это действительно необходимо.