Интерфейс против Базового класса

767

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

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

Если у меня есть класс собак и кошек. Почему я хотел бы реализовать IPet вместо PetBase? Я могу понять наличие интерфейсов для ISheds или IBarks (IMakesNoise?), Потому что они могут быть размещены на основе питомца за питомцем, но я не понимаю, какой использовать для обычного питомца.

Howler
источник
11
Я думаю, вам следует принять во внимание один момент: интерфейсы могут иметь несколько ограничений, о которых вы можете не знать до очень поздних стадий. Например, в .NET вы не можете сериализовать переменную-член интерфейса, поэтому если у вас есть класс Zoo и массив переменных-членов IAnimals, вы не сможете сериализовать Zoo (а это означает, что написание WebServices или других вещей, требующих сериализации, будет боль).
синхершко
1
Этот вопрос может помочь понять концепцию интерфейсов. stackoverflow.com/q/8531292/1055241
gprathour
Мне просто интересно. Я встретил в CLR через C # следующий отрывок: I tend to prefer using the interface technique over the base type technique because the base type technique doesn’t allow the developer to choose the base type that works best in a particular situation.. Я не могу понять, что подразумевается в выдержке. Мы можем создать несколько базовых типов и создать производный тип для любого из них, чтобы разработчик мог выбрать базовый тип. Может кто-нибудь объяснить, пожалуйста, что мне не хватает? Я считаю, что это может быть частью этого вопроса. Или я должен опубликовать еще один о конкретной выдержке?
qqqqqqq

Ответы:

501

Давайте возьмем ваш пример классов Dog и Cat и проиллюстрируем это на C #:

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

public abstract class Mammal

Этот базовый класс, вероятно, будет иметь методы по умолчанию, такие как:

  • Подача
  • Приятель

Все это поведение, которое имеет более или менее одинаковую реализацию для обоих видов. Чтобы определить это у вас будет:

public class Dog : Mammal
public class Cat : Mammal

Теперь предположим, что есть другие млекопитающие, которых мы обычно будем видеть в зоопарке:

public class Giraffe : Mammal
public class Rhinoceros : Mammal
public class Hippopotamus : Mammal

Это все еще будет в силе, потому что в основе функциональности Feed()и Mate()останется прежним.

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

public interface IPettable
{
    IList<Trick> Tricks{get; set;}
    void Bathe();
    void Train(Trick t);
}

Реализация вышеуказанного контракта не будет одинаковой между кошкой и собакой; помещать их реализации в абстрактный класс для наследования будет плохой идеей.

Ваши определения собак и кошек теперь должны выглядеть так:

public class Dog : Mammal, IPettable
public class Cat : Mammal, IPettable

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

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

Jon Limjap
источник
149
Я не думаю, что это так просто. Вы немного изменили вопрос (требования), чтобы интерфейс стал более понятным. Вы должны всегда спрашивать себя, определяете ли вы контракт (интерфейс) или разделяемую реализацию (базовый класс).
Дэвид Поклуда
18
Интерфейс является контрактом. Вы раскрываете только ту часть контракта, которая требуется для услуги. Если у вас есть «PettingZoo», вы, конечно же, не хотите показывать «Mate» пользователю!
Энтони Мастреан
10
@ Дэвид Туш, хотя я сделал это, чтобы лучше проиллюстрировать, для чего предназначен интерфейс и что такое абстрактный класс для его понимания. Собака и кошка не являются жестким требованием!
Джон Лимджап
2
Следует отметить, что в интерпретируемых средах JIT (особенно JVM) вызовы виртуальных методов значительно быстрее вызовов интерфейсных методов , в зависимости от контекста. Я подчеркиваю «контекст», потому что JVM часто может оптимизировать поиск медленных методов. (Например, если список наследников интерфейса, как правило, является экземплярами одного и того же класса. Это, к сожалению, затрудняет сравнительный анализ.) Если вы пытаетесь оптимизировать что-то, чувствительное к производительности, и только тогда, вы должны рассмотреть это.
Филипп Гуин
14
Кроме того, интерфейс допускает скалу для домашних животных, позволяя ей купаться и обучаться трюкам, но кормление и спаривание не будут поддерживаться, потому что это будет смешно для скалы.
eclux
146

Ну, Джош Блох сказал себе в Effective Java 2d :

Предпочитаю интерфейсы абстрактным классам

Некоторые основные моменты:

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

  • Интерфейсы идеально подходят для определения миксинов . Грубо говоря, mixin - это тип, который класс может реализовать в дополнение к своему «первичному типу», чтобы объявить, что он обеспечивает некоторое необязательное поведение. Например, Comparable - это смешанный интерфейс, который позволяет классу объявить, что его экземпляры упорядочены относительно других взаимно сопоставимых объектов.

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

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

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

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

PS .: Купить книгу. Это намного более подробно.

Marcio Aguiar
источник
71
Всякий раз, когда необходимо внести изменение в интерфейс, неразрывный способ сделать это - создать новый интерфейс, который наследуется от старого. Это сохраняет существующие реализации и позволяет вам делать что угодно в новой.
Скотт Лоуренс
5
у нас есть «Принцип разделения интерфейса». Этот принцип учит нас заботиться о том, как мы пишем наши интерфейсы. Когда мы пишем наши интерфейсы, мы должны стараться добавлять только те методы, которые должны быть там. Если мы добавим методы, которых не должно быть, классы, реализующие интерфейс, также должны будут реализовать эти методы. Например, если мы создадим интерфейс с именем Worker и добавим метод перерыв на обед, все работники должны будут его реализовать. Что делать, если работник робот? Как заключение Интерфейсы, содержащие методы, которые не являются специфичными для него, называются загрязненными или толстыми интерфейсами.
Eklavyaa
Начиная с Java 8, методы Default позволяют добавлять новые функциональные возможности к интерфейсам и обеспечивать обратную совместимость для существующих классов, которые реализуют этот интерфейс. Методы по умолчанию вызываются по умолчанию, если они не переопределены в реализующем классе. Все реализующие классы могут либо переопределить методы по умолчанию, либо они могут напрямую вызывать их, используя instance.defaultMethod ()
Arpit Rastogi
118

Интерфейсы и базовые классы представляют две разные формы отношений.

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

Интерфейсы , с другой стороны, представляют дополнительные функции класса. Я бы назвал это «есть», как в « Fooодноразовых», следовательно, IDisposableинтерфейс в C #.

Томас Данекер
источник
10
Из всех ответов этот дает лучшую смесь краткости без потери ясности
Мэтт Вилко
Кто-то однажды сказал мне использовать интерфейс, когда есть отношение «есть». Я не уверен, что это всегда правда; В моем ноутбуке есть экран, поэтому должен ли ноутбук использовать IScreen или иметь свойство Screen? Последнее кажется мне более естественным.
Беренд
1
@ смешно, что вы пишете это, потому что экраны реализованы через интерфейсы - VGA, HDMI и т. д.
Константин
Просто потому, что это ООП, мы должны расширить наше воображение, чтобы следовать реальным сценариям жизни. Это не всегда применимо.
Крисмограмма
110

Современный стиль - это определение IPet и PetBase.

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

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

Джейсон Коэн
источник
есть свой торт и есть его тоже!
Даррен Копп
3
Интерфейс определяет, как другие классы могут использовать ваш код. Базовый класс помогает разработчикам реализовать ваш интерфейс. Две разные вещи для двух разных целей.
Билл Мичелл
27
Здесь нет ничего «современного». Базовый класс и интерфейс с одним и тем же API просто избыточны. Вы можете использовать этот подход в некоторых случаях, но вы не должны обобщать!
smentek
3
Наиболее поддерживаемый комментарий ко второму поддерживаемому ответу на самом деле не согласен с самим ответом, интересно. Должен сказать, что я вижу много примеров интерфейса и базового класса. В этом смысле это «современный» путь. Например, в шаблоне MVVM фактически есть класс ViewModelBase, который реализует INotifyPropertyChanged. Но когда мой коллега спросил меня, почему у меня есть базовый класс, вместо того, чтобы реализовывать интерфейс в каждой модели представления, я не знаю, как его убедить
тет
1
Правильный. Это не вопрос 1 или другой. Они существуют для решения 2 очень разных проблем. Интерфейсы - это контракты, которым должен заниматься реализующий класс. Они нашли недавнюю пользу (иногда фанатично) для аспектов IoC и TDD. Абстрактные / Базовые классы служат для группировки иерархически общей логики и свойств. Это уменьшает дублирующийся код, что, в свою очередь, повышает удобство сопровождения решения и делает его менее подверженным ошибкам.
ComeIn
63

Интерфейсы

  • Определяет договор между 2 модулями. Не может быть никакой реализации.
  • Большинство языков позволяют реализовать несколько интерфейсов
  • Изменение интерфейса является серьезным изменением. Все реализации должны быть перекомпилированы / изменены.
  • Все участники являются публичными. Реализации должны реализовывать все участники.
  • Интерфейсы помогают в развязке. Вы можете использовать макет фреймворков, чтобы макетировать что-нибудь за интерфейсом
  • Интерфейсы обычно указывают на некое поведение
  • Реализации интерфейса отделены / изолированы друг от друга

Базовые классы

  • Позволяет добавить некоторую реализацию по умолчанию, которую вы получаете бесплатно путем деривации
  • За исключением C ++, вы можете наследовать только от одного класса. Даже если может из нескольких классов, это обычно плохая идея.
  • Смена базового класса относительно проста. Производные не нужно делать ничего особенного
  • Базовые классы могут объявлять защищенные и публичные функции, к которым могут обращаться производные
  • Абстрактные Базовые классы не могут быть легко смоделированы как интерфейсы
  • Базовые классы обычно указывают иерархию типов (IS A)
  • Деривация классов может зависеть от некоторого базового поведения (иметь сложные знания родительской реализации). Вещи могут быть грязными, если вы внесете изменения в базовую реализацию для одного парня и сломаете других.
Gishu
источник
Примечание: руководящие принципы разработки структуры рекомендуют использовать базовые классы (в отличие от интерфейсов), потому что они лучше работают. Добавление нового метода к абстрактному базовому классу в vNext - непреложное изменение ..
Гишу,
59

В общем, вы должны отдавать предпочтение интерфейсам перед абстрактными классами. Одна из причин использовать абстрактный класс - если у вас есть общая реализация среди конкретных классов. Конечно, вы все равно должны объявить интерфейс (IPet) и иметь абстрактный класс (PetBase), реализующий этот интерфейс. Используя небольшие отдельные интерфейсы, вы можете использовать множественные числа для дальнейшего повышения гибкости. Интерфейсы обеспечивают максимальную гибкость и переносимость типов через границы. При передаче ссылок через границы всегда передавайте интерфейс, а не конкретный тип. Это позволяет принимающей стороне определить конкретную реализацию и обеспечивает максимальную гибкость. Это абсолютно верно при программировании в стиле TDD / BDD.

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

Килхоффер
источник
Иш. Лично я считаю, что это задница задом наперед. Интерфейсы должны содержать минимальные функциональные возможности для типа, а базовые классы должны обеспечивать богатую структуру, на которой можно построить настройку. Поместите это в интерфейс, и вы только что сделали это очень сложным для реализации.
Никто не говорил, что ваши интерфейсы должны быть огромными. Меньшие, множественные интерфейсы и более богатые базовые классы делают отличный API.
Килхоффер
3
Это только у меня, или у большинства классов "общего рабочего" общая реализация? В этом контексте это идет вразрез с вашим общим правилом предпочтения интерфейсов. Я бы повторил ваше обобщение на 2 обобщения: те общие классы, которые не содержат или имеют небольшую логику, должны реализовывать интерфейс. Те распространенные классы, которые содержат «приличное» количество логики, должны быть производными от базового класса (так как, скорее всего, они будут совместно использовать функциональность).
goku_da_master
@Kilhoffer «Интерфейсы обеспечивают максимальную гибкость и переносимость типов через границы», пожалуйста, уточните это утверждение.
JAVA
49

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

Кшиштоф Квалина говорит на странице 81:

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

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

fryguybob
источник
19

Хуан,

Мне нравится думать об интерфейсах как о способе охарактеризовать класс. Конкретный класс породы собак, например, YorkshireTerrier, может быть потомком родительского класса собак, но он также реализует IFurry, IStubby и IYippieDog. Таким образом, класс определяет, что это за класс, но интерфейс рассказывает нам об этом.

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

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

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

Так что, если вы думаете, как я, вы бы определенно сказали, что Cat и Dog являются IPettable. Это характеристика, которая соответствует им обоим.

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

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

Или они должны быть млекопитающими? Может быть, нам нужен какой-нибудь завод по производству животных?

Они вообще должны быть связаны друг с другом? Достаточно ли просто знать, что они оба IPettable?

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

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

Флори
источник
18

Вот основное и простое определение интерфейса и базового класса:

  • Базовый класс = наследование объекта.
  • Интерфейс = функциональное наследование.

ура

RWendi
источник
12

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

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

Таким образом, вместо IPet или PetBase вы можете получить Pet с параметром IFurBehavior. Параметр IFurBehavior устанавливается методом CreateDog () PetFactory. Именно этот параметр вызывается для метода shed ().

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

Я рекомендую этот шаблон даже в языках с множественным наследованием.

Джо
источник
12

Это хорошо объяснено в этой статье Java World .

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

Нередко у меня будет класс, реализующий один или несколько интерфейсов.

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

Ниже приводится выдержка из вышеупомянутой статьи JavaWorld.com, автор Тони Синтес, 20.04.01


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

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

Абстрактные классы позволяют вам определять некоторые поведения; они заставляют ваши подклассы предоставлять другим. Например, если у вас есть платформа приложения, абстрактный класс может предоставлять службы по умолчанию, такие как обработка событий и сообщений. Эти сервисы позволяют вашему приложению подключаться к вашей структуре приложения. Тем не менее, есть некоторые специфичные для приложения функции, которые может выполнять только ваше приложение. Такая функциональность может включать задачи запуска и завершения работы, которые часто зависят от приложения. Поэтому вместо того, чтобы пытаться определить это поведение, абстрактный базовый класс может объявлять абстрактные методы завершения работы и запуска. Базовый класс знает, что ему нужны эти методы, но абстрактный класс позволяет вашему классу признать, что он не знает, как выполнять эти действия; он только знает, что должен инициировать действия. Когда пора начинать, абстрактный класс может вызывать метод запуска. Когда базовый класс вызывает этот метод, Java вызывает метод, определенный дочерним классом.

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

Класс против интерфейса

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

Например, шаблон «Стратегия» позволяет вам добавлять новые алгоритмы и процессы в вашу программу, не изменяя объекты, которые их используют. Медиаплеер может знать, как воспроизводить CD, MP3 и WAV-файлы. Конечно, вы не хотите жестко закодировать эти алгоритмы воспроизведения в проигрыватель; это затруднит добавление нового формата, такого как AVI. Кроме того, ваш код будет завален бесполезными инструкциями. И чтобы добавить оскорбление к травме, вам нужно будет обновлять эти утверждения случая каждый раз, когда вы добавляете новый алгоритм. В общем, это не очень объектно-ориентированный способ программирования.

С помощью шаблона «Стратегия» вы можете просто инкапсулировать алгоритм за объектом. Если вы это сделаете, вы можете предоставить новые медиа-плагины в любое время. Давайте назовем плагин класса MediaStrategy. Этот объект будет иметь один метод: playStream (Stream s). Таким образом, чтобы добавить новый алгоритм, мы просто расширяем наш класс алгоритма. Теперь, когда программа встречает новый тип медиа, она просто делегирует воспроизведение потока нашей медиа стратегии. Конечно, вам понадобится немного сантехники для правильной реализации алгоритмов стратегий, которые вам понадобятся.

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

Richard Harrison
источник
2
+1. «Опора на наследование для идентификации типа опасна; она блокирует вас в определенной иерархии наследования». Это предложение прекрасно описывает мои причины для предпочтения интерфейсов.
инженер
И помимо этого, вместо расширения составьте реализацию каждого метода вашего интерфейса.
инженер
10

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

Sijin
источник
10

У меня есть грубое эмпирическое правило

Функциональность: вероятно, будет отличаться во всех частях: Интерфейс.

Данные и функциональность, части будут в основном одинаковыми, части разные: абстрактный класс.

Данные и функциональность, реально работающие, если расширены только с небольшими изменениями: обычный (конкретный) класс

Данные и функциональность, никаких изменений не планируется: обычный (конкретный) класс с финальным модификатором.

Данные и, возможно, функциональность: только для чтения: члены enum.

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

Ахиль Джайн
источник
7

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

Абстрактные классы - это ярлыки. Существуют ли вещи, которые разделяют все производные PetBase, которые вы можете кодировать один раз и покончить с ними? Если да, то пришло время для абстрактного класса.

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

Абстрактные классы могут содержать несколько интерфейсов. Ваш абстрактный класс PetBase может реализовывать IPet (домашние животные имеют владельцев) и IDigestion (домашние животные едят или, по крайней мере, должны). Тем не менее, PetBase, вероятно, не будет реализовывать IMammal, поскольку не все домашние животные являются млекопитающими и не все млекопитающие являются домашними животными. Вы можете добавить MammalPetBase, который расширяет PetBase, и добавить IMammal. FishBase может иметь PetBase и добавить IFish. IFish будет иметь ISwim и IUnderwaterBreather в качестве интерфейсов.

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

Джаррет Мейер
источник
7

Источник : http://jasonroell.com/2014/12/09/interfaces-vs-abstract-classes-what-should-you-use/

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

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

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

Это все хорошо и хорошо, и обеспечивает отличное повторное использование кода и придерживается принципа DRY (не повторяй себя) разработки программного обеспечения. Абстрактные классы прекрасно использовать, когда у вас есть отношения «есть».

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

public abstract class Dog
{
      public virtual void Bark()
      {
        Console.WriteLine("Base Class implementation of Bark");
      }
}

public class GoldenRetriever : Dog
{
   // the Bark method is inherited from the Dog class
}

public class Poodle : Dog
{
  // here we are overriding the base functionality of Bark with our new implementation
  // specific to the Poodle class
  public override void Bark()
  {
     Console.WriteLine("Poodle's implementation of Bark");
  }
}

// Add a list of dogs to a collection and call the bark method.

void Main()
{
    var poodle = new Poodle();
    var goldenRetriever = new GoldenRetriever();

    var dogs = new List<Dog>();
    dogs.Add(poodle);
    dogs.Add(goldenRetriever);

    foreach (var dog in dogs)
    {
       dog.Bark();
    }
}

// Output will be:
// Poodle's implementation of Bark
// Base Class implementation of Bark

// 

Как вы можете видеть, это было бы отличным способом сохранить ваш код СУХИМЫМ и позволить вызывать реализацию базового класса, когда любой из типов может просто полагаться на стандартную Bark вместо специальной реализации. Такие классы, как GoldenRetriever, Boxer, Lab, все могут унаследовать Bark «по умолчанию» (класс баса) просто потому, что они реализуют абстрактный класс Dog.

Но я уверен, что вы уже знали это.

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

Что, черт возьми, это значит? Ну, например: человек не утка ... и утка не человек. Довольно очевидно. Однако и утка, и человек обладают способностью плавать (учитывая, что человек прошел свои уроки плавания в 1-м классе :)). Кроме того, поскольку утка - это не человек или наоборот, это не «реализация», а отношения «способный», и мы можем использовать интерфейс, чтобы проиллюстрировать это:

// Create ISwimable interface
public interface ISwimable
{
      public void Swim();
}

// Have Human implement ISwimable Interface
public class Human : ISwimable

     public void Swim()
     {
        //Human's implementation of Swim
        Console.WriteLine("I'm a human swimming!");
     }

// Have Duck implement ISwimable interface
public class Duck: ISwimable
{
     public void Swim()
     {
          // Duck's implementation of Swim
          Console.WriteLine("Quack! Quack! I'm a Duck swimming!")
     }
}

//Now they can both be used in places where you just need an object that has the ability "to swim"

public void ShowHowYouSwim(ISwimable somethingThatCanSwim)
{
     somethingThatCanSwim.Swim();
}

public void Main()
{
      var human = new Human();
      var duck = new Duck();

      var listOfThingsThatCanSwim = new List<ISwimable>();

      listOfThingsThatCanSwim.Add(duck);
      listOfThingsThatCanSwim.Add(human);

      foreach (var something in listOfThingsThatCanSwim)
      {
           ShowHowYouSwim(something);
      }
}

 // So at runtime the correct implementation of something.Swim() will be called
 // Output:
 // Quack! Quack! I'm a Duck swimming!
 // I'm a human swimming!

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

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

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

Резюме:

Поэтому мое основное правило - использовать абстрактный класс, если вы хотите реализовать функциональность «по умолчанию» для иерархии классов и / или классы или типы, с которыми вы работаете, используют отношение «есть» (например, пудель «это Тип собаки).

С другой стороны, используйте интерфейс, когда у вас нет отношений «есть», но есть типы, которые разделяют «способность» что-то делать или что-то иметь (например, утка «не» является человеком. Однако утка и человеческая доля «Умение» плавать).

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

Также помните, что при использовании интерфейсов следует придерживаться принципа разделения интерфейса (ISP). Провайдер заявляет, что ни один клиент не должен зависеть от методов, которые он не использует. По этой причине интерфейсы должны быть ориентированы на конкретные задачи и обычно очень малы (например, IDisposable, IComparable).

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

Надеюсь, это прояснит ситуацию для некоторых людей!

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

Джейсон Роелл
источник
6

Случай с базовыми классами через интерфейсы был хорошо объяснен в Руководстве по кодированию Submain .NET:

Базовые классы и интерфейсы. Тип интерфейса - это частичное описание значения, которое потенциально поддерживается многими типами объектов. По возможности используйте базовые классы вместо интерфейсов. С точки зрения управления версиями классы более гибкие, чем интерфейсы. С помощью класса вы можете отправить версию 1.0, а затем в версии 2.0 добавить новый метод в класс. Пока метод не является абстрактным, любые существующие производные классы продолжают функционировать без изменений.

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

  1. Несколько не связанных классов хотят поддерживать протокол.
  2. Эти классы уже имеют установленные базовые классы (например, некоторые являются элементами управления пользовательского интерфейса (UI), а некоторые - веб-службами XML).
  3. Агрегирование не подходит или практически невозможно. Во всех других ситуациях наследование классов является лучшей моделью.
YeahStu
источник
Я чувствую, что этот ответ должен получить больше внимания. Это идет вразрез с множеством ответов здесь. Я бы не сказал, что полностью согласен, но здесь есть свои плюсы.
kayleeFrye_onDeck
5

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

оборота Джоэл Коухорн
источник
4

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

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

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

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

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

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

Дон Эдвардс
источник
Чисто абстрактный класс может обеспечивать поведение по умолчанию для методов. Это полезно, когда все ваши конкретные классы будут использовать общие методы, которые будут избыточными для повторной реализации снова и снова.
Адам Хьюз
4

Предпочитаю интерфейсы абстрактным классам

Обоснование, основные моменты для рассмотрения [два уже упомянутых здесь] являются:

  • Интерфейсы более гибкие, потому что класс может реализовывать несколько интерфейсов. Поскольку Java не имеет множественного наследования, использование абстрактных классов не позволяет вашим пользователям использовать любую другую иерархию классов. В общем, предпочитайте интерфейсы, когда нет реализаций по умолчанию или состояния. Коллекции Java предлагают хорошие примеры этого (Map, Set и т. Д.).
  • У абстрактных классов есть преимущество, заключающееся в том, что они обеспечивают лучшую прямую совместимость. Когда клиенты используют интерфейс, вы не можете его изменить; если они используют абстрактный класс, вы все равно можете добавить поведение, не нарушая существующий код. Если проблема совместимости, подумайте об использовании абстрактных классов.
  • Даже если у вас есть реализации по умолчанию или внутреннее состояние, рассмотрите возможность предложить интерфейс и его абстрактную реализацию . Это поможет клиентам, но все же предоставит им большую свободу при желании [1].
    Конечно, предмет был подробно обсужден в другом месте [2,3].

[1] Конечно, он добавляет больше кода, но если краткость является вашей главной задачей, вам, вероятно, следовало бы избегать Java в первую очередь!

[2] Джошуа Блох, Эффективная Ява, пункты 16-18.

[3] http://www.codeproject.com/KB/ar ...

Джаймин Патель
источник
3

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

Скотт Лоуренс
источник
3

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

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

Короче говоря, если A «представляет собой» B, то A требует не более B и обеспечивает не менее B для каждого раскрываемого им метода.

theschmitzer
источник
3

Другой вариант, о котором следует помнить, - это использование отношения «имеет», иначе «реализуется в терминах» или «состав». Иногда это более чистый, более гибкий способ структурирования вещей, чем использование наследования "is-a".

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

public class Pet
{
    void Bathe();
    void Train(Trick t);
}

public class Dog
{
    private Pet pet;

    public void Bathe() { pet.Bathe(); }
    public void Train(Trick t) { pet.Train(t); }
}

public class Cat
{
    private Pet pet;

    public void Bathe() { pet.Bathe(); }
    public void Train(Trick t) { pet.Train(t); }
}

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

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

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

Параппа
источник
3

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

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

Классы предоставляют реализацию, и они могут объявить, что они реализуют ноль, один или несколько интерфейсов. Если класс предназначен для наследования, соглашение заключается в добавлении префикса имени класса к «Base».

Существует различие между базовым классом и абстрактным базовым классом (ABC). ABC смешивают интерфейс и реализацию вместе. Абстракция вне компьютерного программирования означает «резюме», то есть «абстрактный == интерфейс». Затем абстрактный базовый класс может описывать как интерфейс, так и пустую, частичную или полную реализацию, которая предназначена для наследования.

Мнения о том, когда использовать интерфейсы или абстрактные базовые классы, а не просто классы , будут сильно различаться в зависимости от того, что вы разрабатываете, и от того, на каком языке вы разрабатываете. Интерфейсы часто ассоциируются только со статически типизированными языками, такими как Java или C #, но Динамически типизированные языки также могут иметь интерфейсы и абстрактные базовые классы . В Python, например, различие становится ясным между классом, который заявляет , что она реализует в интерфейс , и объект, который является экземпляром класса , и , как говорят обеспечить , что интерфейс, На динамическом языке возможно, что два объекта, которые являются экземплярами одного и того же класса , могут объявить, что они предоставляют совершенно разные интерфейсы. В Python это возможно только для атрибутов объекта, в то время как методы являются общим состоянием для всех объектов класса . Однако в Ruby объекты могут иметь методы для каждого экземпляра, поэтому возможно, что интерфейс между двумя объектами одного и того же класса может варьироваться настолько, насколько этого желает программист (однако, Ruby не имеет какого-либо явного способа объявления интерфейсов).

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

пшеницы
источник
2

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

Недостатком реализации базового класса является требование к override(или new) существующим методам. Это делает их виртуальными методами, что означает, что вы должны быть осторожны с тем, как вы используете экземпляр объекта.

Наконец, единственное наследование .NET убивает меня. Наивный пример: скажем, вы делаете пользовательский элемент управления, поэтому вы наследуете UserControl. Но теперь вы заблокированы от наследования PetBase. Это вынуждает вас реорганизоваться, например, чтобы сделать PetBaseчлена класса вместо этого.

spoulson
источник
2

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

Билл Ящерица
источник
2

Что касается C #, то в некотором смысле интерфейсы и абстрактные классы могут быть взаимозаменяемыми. Однако различия заключаются в следующем: i) интерфейсы не могут реализовывать код; ii) из-за этого интерфейсы не могут далее вызывать стек до подкласса; и iii) только абстрактный класс может быть унаследован от класса, тогда как в классе может быть реализовано несколько интерфейсов.

CarneyCode
источник
2

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

Ганеш Кодиганти
источник
2

Я обнаружил, что шаблон Interface> Abstract> Concrete работает в следующем сценарии использования:

1.  You have a general interface (eg IPet)
2.  You have a implementation that is less general (eg Mammal)
3.  You have many concrete members (eg Cat, Dog, Ape)

Абстрактный класс определяет общие атрибуты по умолчанию для конкретных классов, но обеспечивает интерфейс. Например:

public interface IPet{

    public boolean hasHair();

    public boolean walksUprights();

    public boolean hasNipples();
}

Теперь, поскольку у всех млекопитающих есть волосы и соски (AFAIK, я не зоолог), мы можем свернуть это в абстрактный базовый класс

public abstract class Mammal() implements IPet{

     @override
     public walksUpright(){
         throw new NotSupportedException("Walks Upright not implemented");
     }

     @override
     public hasNipples(){return true}

     @override
     public hasHair(){return true}

И тогда конкретные классы просто определяют, что они идут прямо.

public class Ape extends Mammal(){

    @override
    public walksUpright(return true)
}

public class Catextends Mammal(){

    @override
    public walksUpright(return false)
}

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

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

Адам Хьюз
источник
1

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

Аарон Фишер
источник
1

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

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

sunwukung
источник