Интерфейс против Базового класса
Когда я должен использовать интерфейс и когда я должен использовать базовый класс?
Должен ли он всегда быть интерфейсом, если я не хочу определять базовую реализацию методов?
Если у меня есть класс собак и кошек. Почему я хотел бы реализовать IPet вместо PetBase? Я могу понять наличие интерфейсов для ISheds или IBarks (IMakesNoise?), Потому что они могут быть размещены на основе питомца за питомцем, но я не понимаю, какой использовать для обычного питомца.
39 ответов
Давайте возьмем ваш пример классов 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++), но способны реализовывать несколько интерфейсов, это позволяет вам создавать объекты строго по мере необходимости.
Ну, Джош Блох сказал себе в Effective Java 2d:
Предпочитаю интерфейсы абстрактным классам
Некоторые основные моменты:
Существующие классы могут быть легко модифицированы для реализации нового интерфейса. Все, что вам нужно сделать, это добавить требуемые методы, если они еще не существуют, и добавить предложение Implements в объявление класса.
Интерфейсы идеально подходят для определения миксинов. Грубо говоря, mixin - это тип, который класс может реализовать в дополнение к своему "первичному типу", чтобы объявить, что он обеспечивает некоторое необязательное поведение. Например, Comparable - это смешанный интерфейс, который позволяет классу объявлять, что его экземпляры упорядочены относительно других взаимно сопоставимых объектов.
Интерфейсы позволяют создавать структуры неиерархического типа. Иерархии типов отлично подходят для организации некоторых вещей, но другие вещи не попадают аккуратно в жесткую иерархию.
Интерфейсы обеспечивают безопасные, мощные улучшения функциональности через идиому класса обертки. Если вы используете абстрактные классы для определения типов, вы оставляете программиста, который хочет добавить функциональность, без альтернативы, кроме как использовать наследование.
Более того, вы можете комбинировать достоинства интерфейсов и абстрактных классов, предоставляя абстрактный класс реализации скелета для каждого нетривиального интерфейса, который вы экспортируете.
С другой стороны, интерфейсы очень трудно развиваться. Если вы добавите метод к интерфейсу, он сломает все его реализации.
PS.: Купить книгу. Это намного более подробно.
Интерфейсы и базовые классы представляют две разные формы отношений.
Наследование (базовые классы) представляют собой отношения "is-a". Например, собака или кошка - это домашнее животное. Эти отношения всегда представляют (единственную) цель класса (в сочетании с "принципом единой ответственности").
Интерфейсы, с другой стороны, представляют дополнительные функции класса. Я бы назвал это "это" отношения, как в "Foo
одноразовый ", следовательно, IDisposable
интерфейс в C#.
Современный стиль - это определение IPet и PetBase.
Преимущество интерфейса в том, что другой код может использовать его без каких-либо связей с другим исполняемым кодом. Полностью "чистый". Также интерфейсы могут быть смешаны.
Но базовые классы полезны для простых реализаций и общих утилит. Так что предоставьте также абстрактный базовый класс, чтобы сэкономить время и код.
Интерфейсы
- Определяет договор между 2 модулями. Не может иметь никакой реализации.
- Большинство языков позволяют реализовать несколько интерфейсов
- Изменение интерфейса является серьезным изменением. Все реализации должны быть перекомпилированы / изменены.
- Все участники являются публичными. Реализации должны реализовывать все участники.
- Интерфейсы помогают в развязке. Вы можете использовать макет фреймворков, чтобы макетировать что-нибудь за интерфейсом
- Интерфейсы обычно указывают на некое поведение
- Реализации интерфейса отделены / изолированы друг от друга
Базовые классы
- Позволяет добавить некоторую реализацию по умолчанию, которую вы получаете бесплатно путем деривации
- За исключением C++, вы можете наследовать только от одного класса. Даже если можно из нескольких классов, это обычно плохая идея.
- Смена базового класса относительно проста. Производные не нужно делать ничего особенного
- Базовые классы могут объявлять защищенные и публичные функции, к которым могут обращаться производные
- Абстрактные Базовые классы не могут быть легко смоделированы как интерфейсы
- Базовые классы обычно указывают иерархию типов (IS A)
- Деривации классов могут зависеть от некоторого базового поведения (иметь сложные знания родительской реализации). Вещи могут быть грязными, если вы внесете изменения в базовую реализацию для одного парня и сломаете других.
В целом, вы должны отдавать предпочтение интерфейсам перед абстрактными классами. Одна из причин использовать абстрактный класс, если у вас есть общая реализация среди конкретных классов. Конечно, вы все равно должны объявлять интерфейс (IPet) и иметь абстрактный класс (PetBase), реализующий этот интерфейс. Используя небольшие отдельные интерфейсы, вы можете использовать кратные значения для дальнейшего повышения гибкости. Интерфейсы обеспечивают максимальную гибкость и переносимость типов через границы. При передаче ссылок через границы всегда передавайте интерфейс, а не конкретный тип. Это позволяет принимающей стороне определить конкретную реализацию и обеспечивает максимальную гибкость. Это абсолютно верно при программировании в стиле TDD/BDD.
"Брига четырех" заявила в своей книге "Поскольку наследование предоставляет подклассу детали реализации его родителя, часто говорят, что" наследование нарушает инкапсуляцию ". Я верю, что это правда.
Это довольно специфично для.NET, но в книге "Руководство по разработке инфраструктуры" утверждается, что в целом классы предоставляют больше гибкости в развивающейся среде. После доставки интерфейса вы не сможете изменить его, не нарушив код, который использовал этот интерфейс. Однако с помощью класса вы можете изменить его и не нарушать код, ссылающийся на него. Пока вы делаете правильные модификации, включая добавление новых функций, вы сможете расширять и развивать свой код.
Кшиштоф Квалина говорит на странице 81:
В течение трех версий.NET Framework я говорил об этом руководстве с довольно многими разработчиками в нашей команде. Многие из них, в том числе те, кто изначально не соглашался с руководящими принципами, говорили, что сожалеют о том, что поставили какой-то API в качестве интерфейса. Я не слышал ни об одном случае, когда кто-то сожалел, что отправил класс.
При этом, безусловно, есть место для интерфейсов. В качестве общего руководства всегда приводите абстрактную реализацию интерфейса базового класса, если не для чего другого, в качестве примера способа реализации интерфейса. В лучшем случае этот базовый класс сэкономит много работы.
Хуан,
Мне нравится думать об интерфейсах как о способе охарактеризовать класс. Конкретный класс породы собак, скажем, YorkshireTerrier, может быть потомком родительского класса собак, но он также реализует IFurry, IStubby и IYippieDog. Таким образом, класс определяет, что это за класс, но интерфейс рассказывает нам об этом.
Преимущество этого в том, что он позволяет мне, например, собрать все IYippieDog и выбросить их в мою коллекцию Ocean. Так что теперь я могу охватить определенный набор объектов и найти те, которые соответствуют критериям, на которые я смотрю, без слишком тщательного изучения класса.
Я считаю, что интерфейсы действительно должны определять подмножество публичного поведения класса. Если он определяет все общедоступное поведение для всех реализуемых классов, то он обычно не должен существовать. Они не говорят мне ничего полезного.
Эта мысль противоречит идее, что у каждого класса должен быть интерфейс, и вы должны написать код для интерфейса. Это нормально, но вы в конечном итоге получаете множество индивидуальных интерфейсов с классами, и это сбивает с толку. Я понимаю, что идея в том, что на самом деле ничего не стоит делать, и теперь вы можете легко менять местами. Однако я нахожу, что я делаю это редко. Большую часть времени я просто изменяю существующий класс на месте, и у меня возникают те же самые проблемы, которые я всегда делал, если требуется изменить публичный интерфейс этого класса, за исключением того, что теперь мне нужно изменить его в двух местах.
Так что, если вы думаете, как я, вы бы определенно сказали, что Cat и Dog являются IPettable. Это характеристика, которая соответствует им обоим.
Другая часть этого, хотя, должны ли они иметь тот же базовый класс? Вопрос в том, должны ли они рассматриваться в целом как одно и то же. Конечно, они оба Животные, но соответствует ли это тому, как мы собираемся использовать их вместе.
Скажем, я хочу собрать все классы животных и положить их в мой контейнер с ковчегом.
Или они должны быть млекопитающими? Может быть, нам нужен какой-нибудь завод по производству животных?
Они вообще должны быть связаны друг с другом? Достаточно ли просто знать, что они оба IPettable?
Я часто испытываю желание вывести целую иерархию классов, когда мне действительно нужен только один класс. Я делаю это в ожидании, когда-нибудь мне это может понадобиться, и обычно я никогда этого не делаю. Даже когда я это делаю, я обычно обнаруживаю, что мне нужно многое сделать, чтобы это исправить. Это потому, что первый класс, который я создаю, - это не собака, мне не так повезло, это вместо утконоса. Теперь вся моя иерархия классов основана на странном случае, и у меня много потерянного кода.
Вы также можете обнаружить, что не все кошки являются IPettable (как этот безволосый). Теперь вы можете переместить этот интерфейс ко всем подходящим производным классам. Вы обнаружите, что гораздо менее серьезное изменение, что внезапно Cats больше не являются производными от PettableBase.
Вот основное и простое определение интерфейса и базового класса:
- Базовый класс = наследование объекта.
- Интерфейс = функциональное наследование.
ура
Хорошо объяснено в этой статье Java World
Лично я склонен использовать интерфейсы для определения интерфейсов - то есть частей системного дизайна, которые определяют, как к чему-то получить доступ.
Нередко у меня будет класс, реализующий 1 или более интерфейсов.
Абстрактные классы я использую как основу для чего-то другого.
Ниже приводится выдержка из вышеупомянутой статьи JavaWorld.com, автор Тони Синтес, 20.04.01
Интерфейс против абстрактного класса
Выбор интерфейсов и абстрактных классов не является предложением. Если вам нужно изменить свой дизайн, сделайте его интерфейсом. Однако у вас могут быть абстрактные классы, которые обеспечивают некоторое поведение по умолчанию. Абстрактные классы являются отличными кандидатами внутри каркасов приложений.
Абстрактные классы позволяют вам определять некоторые поведения; они заставляют ваши подклассы предоставлять другим. Например, если у вас есть среда приложения, абстрактный класс может предоставлять службы по умолчанию, такие как обработка событий и сообщений. Эти сервисы позволяют вашему приложению подключаться к вашей структуре приложения. Тем не менее, есть некоторые специфичные для приложения функции, которые может выполнять только ваше приложение. Такая функциональность может включать задачи запуска и завершения работы, которые часто зависят от приложения. Поэтому вместо того, чтобы пытаться определить это поведение самостоятельно, абстрактный базовый класс может объявлять абстрактные методы завершения работы и запуска. Базовый класс знает, что ему нужны эти методы, но абстрактный класс позволяет вашему классу признать, что он не знает, как выполнять эти действия; он только знает, что должен инициировать действия. Когда приходит время запуска, абстрактный класс может вызывать метод запуска. Когда базовый класс вызывает этот метод, Java вызывает метод, определенный дочерним классом.
Многие разработчики забывают, что класс, который определяет абстрактный метод, может также вызывать этот метод. Абстрактные классы являются отличным способом создания плановых иерархий наследования. Они также являются хорошим выбором для нелистовых классов в иерархиях классов.
Класс против интерфейса
Некоторые говорят, что вы должны определить все классы с точки зрения интерфейсов, но я думаю, что рекомендации кажутся немного экстремальными. Я использую интерфейсы, когда вижу, что что-то в моем дизайне будет часто меняться.
Например, шаблон "Стратегия" позволяет вам добавлять новые алгоритмы и процессы в вашу программу, не изменяя объекты, которые их используют. Медиаплеер может знать, как воспроизводить CD, MP3 и WAV файлы. Конечно, вы не хотите жестко закодировать эти алгоритмы воспроизведения в проигрыватель; это затруднит добавление нового формата, такого как AVI. Кроме того, ваш код будет завален бесполезными инструкциями. И чтобы добавить оскорбление к травме, вам нужно будет обновлять эти утверждения случая каждый раз, когда вы добавляете новый алгоритм. В общем, это не очень объектно-ориентированный способ программирования.
С помощью шаблона "Стратегия" вы можете просто инкапсулировать алгоритм за объектом. Если вы это сделаете, вы можете предоставить новые медиа-плагины в любое время. Давайте назовем плагин класса MediaStrategy. Этот объект будет иметь один метод: playStream(Stream s). Таким образом, чтобы добавить новый алгоритм, мы просто расширяем наш класс алгоритма. Теперь, когда программа встречает новый тип медиа, она просто делегирует воспроизведение потока нашей медиа стратегии. Конечно, вам понадобится немного сантехники для правильной реализации алгоритмов стратегий, которые вам понадобятся.
Это отличное место для использования интерфейса. Мы использовали шаблон "Стратегия", который четко указывает место в дизайне, которое изменится. Таким образом, вы должны определить стратегию как интерфейс. Обычно вы должны отдавать предпочтение интерфейсам, а не наследованию, когда хотите, чтобы объект имел определенный тип; в этом случае MediaStrategy. Опора на наследование для идентификации типа опасна; это блокирует вас в определенной иерархии наследования. Java не допускает множественного наследования, поэтому вы не можете расширять что-то, что дает вам полезную реализацию или дополнительную идентификацию типов.
Также имейте в виду, что в OO не смейтесь ( см. Блог) и всегда моделируйте объекты, основываясь на требуемом поведении. Если вы разрабатываете приложение, в котором вам требуется только общее название и вид животного, то вам нужно только один класс Animal со свойством для имени, вместо миллионов классов для каждого возможного животного в мире.
Я рекомендую использовать композицию вместо наследования, когда это возможно. Используйте интерфейсы, но используйте объекты-члены для базовой реализации. Таким образом, вы можете определить фабрику, которая будет конструировать ваши объекты так, чтобы они вели себя определенным образом. Если вы хотите изменить поведение, вы создаете новый фабричный метод (или абстрактную фабрику), который создает различные типы подобъектов.
В некоторых случаях вы можете обнаружить, что вашим основным объектам вообще не нужны интерфейсы, если все изменяемое поведение определено в вспомогательных объектах.
Таким образом, вместо IPet или PetBase вы можете получить Pet с параметром IFurBehavior. Параметр IFurBehavior устанавливается методом CreateDog() PetFactory. Именно этот параметр вызывается для метода shed().
Если вы сделаете это, вы обнаружите, что ваш код гораздо более гибок, и большинство ваших простых объектов имеют дело с очень базовым поведением всей системы.
Я рекомендую этот шаблон даже в языках множественного наследования.
Источник: 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).
Другой совет, если вы разрабатываете небольшие, краткие кусочки функциональности, используйте интерфейсы. Если вы разрабатываете большие функциональные блоки, используйте абстрактный класс.
Надеюсь, это прояснит ситуацию для некоторых людей!
Также, если вы можете придумать какие-нибудь лучшие примеры или хотите указать что-то, пожалуйста, сделайте это в комментариях ниже!
У меня есть грубое эмпирическое правило
Функциональность: вероятно, будет отличаться во всех частях: Интерфейс.
Данные и функциональность, части будут в основном одинаковыми, части разные: абстрактный класс.
Данные и функциональность, реально работающие, если расширены только с небольшими изменениями: обычный (конкретный) класс
Данные и функциональность, никаких изменений не планируется: обычный (конкретный) класс с финальным модификатором.
Данные и, возможно, функциональность: только для чтения: члены enum.
Это очень грубо и готово и совсем не строго определено, но есть спектр интерфейсов, где все должно быть изменено на перечисления, где все исправлено немного как файл только для чтения.
Интерфейсы должны быть маленькими. Действительно маленький. Если вы действительно разбиваете свои объекты, то ваши интерфейсы, вероятно, будут содержать только несколько очень специфических методов и свойств.
Абстрактные классы - это ярлыки. Существуют ли вещи, которые разделяют все производные PetBase, которые вы можете кодировать один раз и покончить с ними? Если да, то пришло время для абстрактного класса.
Абстрактные классы также ограничены. В то время как они дают вам быстрый способ создания дочерних объектов, любой данный объект может реализовать только один абстрактный класс. Много раз я нахожу это ограничением абстрактных классов, и именно поэтому я использую много интерфейсов.
Абстрактные классы могут содержать несколько интерфейсов. Ваш абстрактный класс PetBase может реализовывать IPet (домашние животные имеют владельцев) и IDigestion (домашние животные едят или, по крайней мере, должны). Тем не менее, PetBase, вероятно, не будет реализовывать IMammal, поскольку не все домашние животные являются млекопитающими и не все млекопитающие являются домашними животными. Вы можете добавить MammalPetBase, который расширяет PetBase, и добавить IMammal. FishBase может иметь PetBase и добавить IFish. IFish будет иметь ISwim и IUnderwaterBreather в качестве интерфейсов.
Да, мой пример чрезмерно сложен для простого примера, но это является отличной особенностью совместной работы интерфейсов и абстрактных классов.
Случай для базовых классов через интерфейсы был хорошо объяснен в Submain .NET Coding Guidelines:
Базовые классы и интерфейсы. Тип интерфейса - это частичное описание значения, которое потенциально поддерживается многими типами объектов. По возможности используйте базовые классы вместо интерфейсов. С точки зрения управления версиями классы более гибкие, чем интерфейсы. С помощью класса вы можете отправить версию 1.0, а затем в версии 2.0 добавить новый метод в класс. Пока метод не является абстрактным, любые существующие производные классы продолжают функционировать без изменений.
Поскольку интерфейсы не поддерживают наследование реализации, шаблон, который применяется к классам, не применяется к интерфейсам. Добавление метода в интерфейс эквивалентно добавлению абстрактного метода в базовый класс; любой класс, который реализует интерфейс, сломается, потому что класс не реализует новый метод. Интерфейсы подходят в следующих ситуациях:
- Несколько несвязанных классов хотят поддерживать протокол.
- Эти классы уже имеют установленные базовые классы (например, некоторые являются элементами управления пользовательского интерфейса (UI), а некоторые - веб-службами XML).
- Агрегирование не подходит или практически невозможно. Во всех других ситуациях наследование классов является лучшей моделью.
Одно важное отличие состоит в том, что вы можете наследовать только один базовый класс, но вы можете реализовать много интерфейсов. Таким образом, вы хотите использовать базовый класс, только если вы абсолютно уверены, что вам не нужно также наследовать другой базовый класс. Кроме того, если вы обнаружите, что ваш интерфейс становится большим, вы должны начать разбивать его на несколько логических частей, которые определяют независимую функциональность, поскольку нет правила, что ваш класс не может реализовать их все (или что вы можете определить другой интерфейс, который просто наследует их все для группировки).
Когда я впервые начал изучать объектно-ориентированное программирование, я совершил легкую и, вероятно, распространенную ошибку, используя наследование, чтобы разделить общее поведение - даже там, где это поведение не было существенным для природы объекта.
Чтобы продолжить опираться на пример, который часто используется в этом конкретном вопросе, есть много вещей, которые можно назвать любимыми - подруги, машины, нечеткие одеяла... - так что у меня мог бы быть класс Petable, обеспечивающий это общее поведение, и различные классы, наследующие от него.
Тем не менее, быть пригодным для питья не является частью природы любого из этих объектов. Существуют гораздо более важные понятия, которые важны для их характера: девушка - это человек, автомобиль - это наземный транспорт, кот - это млекопитающее...
Поведения должны быть сначала назначены интерфейсам (включая интерфейс класса по умолчанию) и повышены до базового класса, только если они (а) являются общими для большой группы классов, которые являются подмножествами более крупного класса - в том же смысле, что "кошка" и "человек" являются подмножествами "млекопитающих".
Суть в том, что после того, как вы поймете, что объектно-ориентированное проектирование достаточно хорошо, чем я сначала, вы обычно будете делать это автоматически, даже не задумываясь Таким образом, чистая правда утверждения "код для интерфейса, а не абстрактного класса" становится настолько очевидной, что вам трудно поверить, что кто-то потрудится сказать это - и начать пытаться воспринимать в нем другие значения.
Еще я хотел бы добавить, что если класс является чисто абстрактным - без неабстрактных, не наследуемых членов или методов, предоставляемых потомку, родителю или клиенту, - тогда почему он является классом? Его можно заменить, в некоторых случаях, интерфейсом, а в других случаях - нулем.
Предпочитаю интерфейсы абстрактным классам
Обоснование, основные моменты для рассмотрения [два уже упомянутых здесь] являются:
- Интерфейсы более гибкие, потому что класс может реализовывать несколько интерфейсов. Поскольку Java не имеет множественного наследования, использование абстрактных классов не позволяет вашим пользователям использовать любую другую иерархию классов. В общем, предпочитайте интерфейсы, когда нет реализаций по умолчанию или состояния. Коллекции Java предлагают хорошие примеры этого (Map, Set и т. Д.).
- У абстрактных классов есть преимущество, заключающееся в улучшении прямой совместимости. Когда клиенты используют интерфейс, вы не можете его изменить; если они используют абстрактный класс, вы все равно можете добавить поведение, не нарушая существующий код. Если проблема совместимости, подумайте об использовании абстрактных классов.
- Даже если у вас есть реализации по умолчанию или внутреннее состояние,рассмотрите возможность предложить интерфейс и его абстрактную реализацию. Это поможет клиентам, но все же предоставит им большую свободу при желании [1].
Конечно, предмет был подробно обсужден в другом месте [2,3].
[1] Конечно, он добавляет больше кода, но если краткость является вашей главной задачей, вам, вероятно, следовало бы избегать Java в первую очередь!
[2] Джошуа Блох, Эффективная Ява, пункты 16-18.
Другой вариант, о котором следует помнить, - это использование отношения "есть-а", иначе "реализовано в терминах" или "состав". Иногда это более чистый и более гибкий способ структурирования, чем использование наследования "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); }
}
Да, этот пример показывает, что существует много дублирования кода и недостатка элегантности, связанных с такими действиями. Но следует также понимать, что это помогает сохранить связь между Собакой и Кошкой от класса Pet (в этом случае у Собаки и Кошки нет доступа к частным членам Pet), и это оставляет место для Собаки и Кошки, чтобы наследовать от чего-то другого. - Возможно, класс Млекопитающих.
Композиция предпочтительна, когда не требуется частный доступ, и вам не нужно ссылаться на собак и кошек, используя общие ссылки / указатели на домашних животных. Интерфейсы предоставляют вам такую общую справочную возможность и могут помочь сократить многословность вашего кода, но они также могут запутывать вещи, когда они плохо организованы. Наследование полезно, когда вам нужен личный доступ, и при его использовании вы берете на себя обязательство связывать свои классы собак и кошек с вашими домашними животными, что требует больших затрат.
Между наследованием, композицией и интерфейсами не существует единственного правильного способа, и это помогает понять, как все три варианта могут использоваться в гармонии. Из трех типов наследование обычно является вариантом, который следует использовать реже всего.
Не используйте базовый класс, если вы не знаете, что он означает и что он применяется в этом случае. Если это применимо, используйте его, иначе используйте интерфейсы. Но обратите внимание на ответ о небольших интерфейсах.
Публичное наследование чрезмерно используется в OOD и выражает гораздо больше, чем большинство разработчиков осознают или хотят соответствовать. См. Принцип замещения Лискова
Короче говоря, если A "представляет собой" B, то A требует не более B и обеспечивает не менее B для каждого раскрываемого им метода.
Концептуально интерфейс используется для формального и полуформального определения набора методов, которые будет предоставлять объект. Формально означает набор имен методов и сигнатур, полуформально означает удобочитаемую документацию, связанную с этими методами. Интерфейсы - это только описания API (в конце концов, API означает Интерфейс прикладного программиста), они не могут содержать какую-либо реализацию, и невозможно использовать или запускать интерфейс. Они только делают явный договор о том, как вы должны взаимодействовать с объектом.
Классы предоставляют реализацию, они могут объявить, что они реализуют ноль, один или несколько интерфейсов. Если класс предназначен для наследования, соглашение заключается в добавлении префикса имени класса к "Base".
Существует различие между базовым классом и абстрактным базовым классом (ABC). ABC смешивают интерфейс и реализацию вместе. Аннотация за пределами компьютерного программирования означает "резюме", то есть "Аннотация == Интерфейс". Затем абстрактный базовый класс может описывать как интерфейс, так и пустую, частичную или полную реализацию, которая предназначена для наследования.
Мнения о том, когда использовать интерфейсы или абстрактные базовые классы, а не только классы, будут сильно различаться в зависимости от того, что вы разрабатываете, и от того, на каком языке вы разрабатываете. Интерфейсы часто ассоциируются только со статически типизированными языками, такими как Java или C#, но Динамически типизированные языки также могут иметь интерфейсы и абстрактные базовые классы. Например, в Python различие проводится между классом, который объявляет, что он реализует интерфейс, и объектом, который является экземпляром класса, и, как говорят, обеспечивает этот интерфейс. На динамическом языке возможно, что два объекта, которые являются экземплярами одного и того же класса, могут объявить, что они предоставляют совершенно разные интерфейсы. В Python это возможно только для атрибутов объекта, в то время как методы являются общим состоянием для всех объектов класса. Однако в Ruby объекты могут иметь методы для каждого экземпляра, поэтому вполне возможно, что интерфейс между двумя объектами одного и того же класса может варьироваться настолько, насколько этого желает программист (однако в Ruby нет никакого явного способа объявления интерфейсов).
В динамических языках Интерфейс к объекту часто неявно предполагается, либо путем интроспекции объекта и запроса его, какие методы он предоставляет (Look Before You Leap), либо, предпочтительно, просто пытаясь использовать требуемый интерфейс для объекта и перехватывая исключения, если объект не предоставляет такой интерфейс (проще просить прощения, чем разрешения). Это может привести к "ложным срабатываниям", когда два интерфейса имеют одинаковое имя метода, но семантически различаются, однако компромисс заключается в том, что ваш код более гибок, так как вам не нужно чрезмерно указывать заранее, чтобы предвидеть все возможные варианты использования вашего кода.
Предыдущие комментарии об использовании абстрактных классов для общей реализации, безусловно, на высоте. Одно из преимуществ, о которых я еще не упоминал, заключается в том, что использование интерфейсов значительно упрощает реализацию фиктивных объектов для целей модульного тестирования. Определение IPet и PetBase, как описано Джейсоном Коэном, позволяет легко смоделировать различные условия данных без затрат на физическую базу данных (пока вы не решите, что пришло время протестировать реальные вещи).
Я обнаружил, что шаблон 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)
}
Этот дизайн хорош, когда есть много конкретных классов, и вы не хотите поддерживать шаблон просто для программирования интерфейса. Если бы новые методы были добавлены к интерфейсу, это сломало бы все получающиеся классы, таким образом, вы все еще получаете преимущества интерфейсного подхода.
В этом случае реферат также может быть конкретным; однако абстрактное обозначение помогает подчеркнуть, что этот образец используется.
Что касается C#, то в некотором смысле интерфейсы и абстрактные классы могут быть взаимозаменяемыми. Однако различия заключаются в следующем: i) интерфейсы не могут реализовывать код; ii) из-за этого интерфейсы не могут далее вызывать стек до подкласса; и iii) только абстрактный класс может быть унаследован от класса, тогда как в классе может быть реализовано несколько интерфейсов.
По умолчанию интерфейс обеспечивает уровень для связи с другим кодом. Все открытые свойства и методы класса по умолчанию реализуют неявный интерфейс. Мы также можем определить интерфейс как роль, когда любой класс должен играть эту роль, он должен реализовать его, предоставляя ему различные формы реализации в зависимости от класса, реализующего его. Следовательно, когда вы говорите об интерфейсе, вы говорите о полиморфизме, а когда вы говорите о базовом классе, вы говорите о наследовании. Два понятия упс!!!
Я обычно тоже не реализую, пока не понадоблюсь. Я предпочитаю интерфейсы абстрактным классам, потому что это дает немного больше гибкости. Если в некоторых классах наследования есть общее поведение, я поднимаю его и создаю абстрактный базовый класс. Я не вижу необходимости в том и другом, поскольку они, по сути, выполняют одну и ту же задачу, а наличие обоих - неприятный запах кода (imho), что решение было чрезмерно разработанным.
Это зависит от ваших требований. Если IPet достаточно прост, я бы предпочел реализовать это. В противном случае, если PetBase реализует массу функциональных возможностей, которые вы не хотите дублировать, используйте их.
Недостатком реализации базового класса является требование override
(или же new
) существующие методы. Это делает их виртуальными методами, что означает, что вы должны быть осторожны с тем, как вы используете экземпляр объекта.
Наконец, единственное наследование.NET убивает меня. Наивный пример: скажем, вы делаете пользовательский элемент управления, поэтому вы наследуете UserControl
, Но теперь вы заблокированы также от наследования PetBase
, Это заставляет вас реорганизовать, например, сделать PetBase
вместо этого член класса.
@Joel: некоторые языки (например, C++) допускают множественное наследование.
Составьте список того, что ваши объекты должны быть, иметь или делать, и что ваши объекты могут (или могут) быть, иметь или делать. Должен указывать ваши базовые типы и может указывать ваши интерфейсы.
Например, ваш PetBase должен дышать, а ваш IPet может делать DoTricks.
Анализ вашей проблемной области поможет вам определить точную иерархическую структуру.