Мой коллега придумал эмпирическое правило для выбора между созданием базового класса или интерфейса.
Он говорит:
Представьте себе каждый новый метод, который вы собираетесь реализовать. Для каждого из них рассмотрим следующее: будет ли этот метод реализован более чем одним классом именно в этой форме, без каких-либо изменений? Если ответ «да», создайте базовый класс. В любой другой ситуации создайте интерфейс.
Например:
Рассмотрим классы
cat
иdog
, расширяющие классmammal
и имеющие один методpet()
. Затем мы добавляем классalligator
, который ничего не расширяет и имеет единственный методslither()
.Теперь мы хотим добавить
eat()
метод для всех них.Если реализация
eat()
метода будет точно такой же дляcat
,dog
иalligator
мы должны создать базовый класс (скажем , давайте,animal
), которая реализует этот метод.Однако, если его реализация
alligator
отличается незначительно, мы должны создатьIEat
интерфейс и создатьmammal
иalligator
реализовать его.
Он настаивает на том, что этот метод охватывает все случаи, но мне кажется, что это упрощение.
Стоит ли следовать этому эмпирическому правилу?
источник
alligator
Реализацияeat
отличается, конечно, тем, что она принимаетcat
иdog
параметры.is a
а интерфейсы -acts like
илиis
. Итак, собакаis a
млекопитающее иacts like
едок. Это говорит нам о том, что млекопитающее должно быть классом, а едок - интерфейсом. Это всегда было очень полезным руководством. Sidenote: примеромis
будетThe cake is eatable
илиThe book is writable
.Ответы:
Я не думаю, что это хорошее эмпирическое правило. Если вас беспокоит повторное использование кода, вы можете
PetEatingBehavior
определить, какая роль заключается в определении функций питания для кошек и собак. Тогда вы можете иметьIEat
и повторно использовать код вместе.В эти дни я вижу все меньше и меньше причин использовать наследство. Одним из больших преимуществ является простота использования. Возьмите графический интерфейс. Популярный способ разработки API для этого заключается в предоставлении огромного базового класса и документирования методов, которые пользователь может переопределить. Таким образом, мы можем игнорировать все другие сложные вещи, которыми должно управлять наше приложение, так как базовый класс предоставляет реализацию по умолчанию. Но мы все еще можем настроить вещи, переопределив правильный метод, когда нам нужно.
Если вы придерживаетесь интерфейса для вашего API, у вашего пользователя обычно есть больше работы, чтобы простые примеры работали. Но API с интерфейсами часто менее связаны и их легче поддерживать по простой причине, которая
IEat
содержит меньше информации, чемMammal
базовый класс. Таким образом, потребители будут зависеть от более слабого контракта, вы получите больше возможностей для рефакторинга и т. Д.Процитирую Rich Hickey: Simple! = Easy
источник
PetEatingBehavior
реализации?PetEatingBehavior
что, вероятно, не тот. Я не могу дать вам реализацию, так как это всего лишь игрушечный пример. Но я могу описать этапы рефакторинга: у него есть конструктор, который берет всю информацию, используемую кошками / собаками для определенияeat
метода (экземпляр зубов, экземпляр желудка и т. Д.). Он содержит только код, общий для кошек и собак, используемый в ихeat
методе. Вам просто нужно создатьPetEatingBehavior
участника и перенаправить его вызовы на dog.eat/cat.eat.Это хорошее практическое правило, но я знаю немало мест, где я его нарушаю.
Честно говоря, я использую (абстрактные) базовые классы гораздо чаще, чем мои сверстники. Я делаю это как защитное программирование.
Для меня (во многих языках) абстрактный базовый класс добавляет ключевое ограничение, что может быть только один базовый класс. Выбирая использовать базовый класс, а не интерфейс, вы говорите: «Это базовая функциональность класса (и любого наследника), и неуместно смешивать ее с другими функциями». Интерфейсы противоположны: «Это некоторая черта объекта, не обязательно его основная ответственность».
Подобные варианты дизайна создают неявные руководства для ваших разработчиков о том, как они должны использовать ваш код, и, соответственно, писать свой код. Из-за их большего влияния на кодовую базу, я склонен принимать во внимание эти вещи при принятии решения о дизайне.
источник
Проблема с упрощением вашего друга заключается в том, что определить, нужно ли когда-либо менять метод, исключительно сложно. Лучше использовать правило, которое говорит о менталитете за суперклассами и интерфейсами.
Вместо того, чтобы выяснить, что вы должны делать, взглянув на то, что вы думаете о функциональности, посмотрите на иерархию, которую вы пытаетесь создать.
Вот как учил мой профессор:
Суперклассы должны быть
is a
и интерфейсы естьacts like
илиis
. Итак, собакаis a
млекопитающее иacts like
едок. Это говорит нам о том, что млекопитающее должно быть классом, а едок - интерфейсом. Примеромis
может бытьThe cake is eatable
илиThe book is writable
(создание обоихeatable
иwritable
интерфейсов).Использование такой методологии достаточно просто и легко и заставляет вас кодировать структуру и концепции, а не то, что, по вашему мнению, будет делать код. Это облегчает обслуживание, делает код более читабельным и упрощает создание дизайна.
Независимо от того, что код может на самом деле сказать, если вы используете такой метод, вы можете вернуться через пять лет и использовать тот же метод, чтобы проследить ваши шаги и легко выяснить, как была разработана ваша программа и как она взаимодействует.
Просто мои два цента.
источник
По просьбе exizt я расширяю свой комментарий для более длинного ответа.
Самая большая ошибка, которую допускают люди, - верить, что интерфейсы - это просто пустые абстрактные классы. Интерфейс - это способ сказать программисту: «Мне все равно, что вы мне дадите, если он следует этому соглашению».
Библиотека .NET служит прекрасным примером. Например, когда вы пишете функцию, которая принимает
IEnumerable<T>
, то, что вы говорите: «Мне все равно, как вы храните свои данные. Я просто хочу знать, что я могу использоватьforeach
цикл.Это приводит к некоторому очень гибкому коду. Внезапно, чтобы быть интегрированным, все, что вам нужно сделать, это играть по правилам существующих интерфейсов. Если реализация интерфейса затруднительна или сбивает с толку, возможно, это намек на то, что вы пытаетесь засунуть квадратный колышек в круглое отверстие.
Но затем возникает вопрос: «А как насчет повторного использования кода? Мои преподаватели CS сказали мне, что наследование было решением всех проблем повторного использования кода и что наследование позволяет вам писать один раз и использовать везде, и это вылечит менангит в процессе спасения сирот». из восходящих морей и не было бы больше слез и так далее и тому подобное и т. д. и т. д. "
Использование наследования только потому, что вам нравится звучание слов «повторное использование кода», является довольно плохой идеей. Руководство по стилю кода от Google делает этот момент довольно лаконичным:
Чтобы проиллюстрировать, почему наследование не всегда является ответом, я собираюсь использовать класс MySpecialFileWriter †. Человек, который слепо верит, что наследование является решением всех проблем, будет утверждать, что вы должны пытаться наследовать
FileStream
, чтобы не дублироватьFileStream
код. Умные люди признают, что это глупо. Вы должны просто иметьFileStream
объект в своем классе (как локальную переменную или переменную-член) и использовать его функциональность.FileStream
Пример может показаться надуманным, но это не так . Если у вас есть два класса, которые оба одинаково реализуют один и тот же интерфейс, то у вас должен быть третий класс, который инкапсулирует любую дублированную операцию. Ваша цель должна состоять в том, чтобы написать классы, которые являются автономными многократно используемыми блоками, которые могут быть собраны вместе, как legos.Это не означает, что наследства следует избегать любой ценой. Есть много моментов, которые нужно рассмотреть, и большинство из них будет рассмотрено при исследовании вопроса «Композиция против наследования». У нашего собственного Stack Overflow есть несколько хороших ответов на эту тему.
В конце концов, настроениям вашего коллеги не хватает глубины или понимания, необходимых для принятия взвешенного решения. Исследуйте предмет и выясните это для себя.
† При иллюстрации наследования каждый использует животных. Это бесполезно. За 11 лет разработки я никогда не писал класс с именем
Cat
, поэтому я не собираюсь использовать его в качестве примера.источник
FileStream
)Примеры редко дают понять, особенно если речь идет об автомобилях или животных. Способ питания животного (или обработки пищи) на самом деле не связан с его наследованием, и не существует единого способа питания, который применим ко всем животным. Это больше зависит от его физических возможностей (так сказать, способов ввода, обработки и вывода). Млекопитающие могут быть, например, плотоядными, травоядными или всеядными, тогда как птицы, млекопитающие и рыбы могут питаться насекомыми. Такое поведение может быть зафиксировано с помощью определенных шаблонов.
В этом случае вам лучше абстрагироваться от поведения, например:
Или внедрите шаблон посетителя, в котором он
FoodProcessor
пытается накормить совместимых животных (ICarnivore : IFoodEater
,MeatProcessingVisitor
). Мысль о живых животных может помешать вам задуматься о том, чтобы разорвать их пищеварительный тракт и заменить его на общий, но вы действительно делаете им одолжение.Это сделает ваше животное базовым классом, и, что еще лучше, вам больше никогда не придется менять его при добавлении нового типа пищи и соответствующего процессора, так что вы можете сосредоточиться на том, что делает этот конкретный класс животных вашим работает над таким уникальным.
Так что да, базовые классы имеют свое место, эмпирическое правило применяется (часто, не всегда, так как это практическое правило), но продолжайте искать поведение, которое вы можете изолировать.
источник
IConsumer
интерфейса. Плотоядные могут потреблять мясо, травоядные - растения, а растения - солнечный свет и воду.Интерфейс - это действительно контракт. Там нет функциональности. Это зависит от разработчика. Там нет выбора, так как нужно выполнять контракт.
Абстрактные классы предоставляют разработчикам выбор. Можно выбрать метод, который должен реализовать каждый, предоставить метод с базовой функциональностью, которую можно переопределить, или предоставить базовую функциональность, которую могут использовать все наследники.
Например:
Я бы сказал, что ваше эмпирическое правило может быть обработано и базовым классом. В частности, поскольку многие млекопитающие, вероятно, «едят» одно и то же, тогда использование базового класса может быть лучше, чем интерфейс, поскольку можно реализовать виртуальный метод поедания, который может использовать большинство наследников, а затем исключительные случаи могут переопределить.
Теперь, если определение «поедания» сильно варьируется от животного к животному, то, возможно, и интерфейс будет лучше. Иногда это серая зона и зависит от конкретной ситуации.
источник
Нет.
Интерфейсы о том, как методы реализованы. Если вы основываете свое решение о том, когда использовать интерфейс, как методы будут реализованы, то вы делаете что-то не так.
Для меня разница между интерфейсом и базовым классом заключается в том, где стоят требования. Вы создаете интерфейс, когда некоторый фрагмент кода требует класса с определенным API и подразумеваемым поведением, которое возникает из этого API. Вы не можете создать интерфейс без кода, который будет его вызывать.
С другой стороны, базовые классы обычно подразумеваются как способ повторного использования кода, поэтому вы редко беспокоитесь о коде, который вызовет этот базовый класс и учитывает только иерархию объектов, частью которых является этот базовый класс.
источник
eat()
в каждое животное? Мы можем сделатьmammal
это, не так ли? Я понимаю вашу точку зрения о требованиях, хотя.Это не очень хорошее эмпирическое правило, потому что если вам нужны разные реализации, вы всегда можете иметь абстрактный базовый класс с абстрактным методом. Каждый наследник гарантированно имеет этот метод, но каждый определяет свою собственную реализацию. Технически у вас может быть абстрактный класс, содержащий только набор абстрактных методов, и он будет в основном таким же, как интерфейс.
Базовые классы полезны, когда вам нужна фактическая реализация некоторого состояния или поведения, которое можно использовать в нескольких классах. Если вы обнаружите, что у вас есть несколько классов с одинаковыми полями и методами с одинаковыми реализациями, вы, вероятно, можете преобразовать это в базовый класс. Вам просто нужно быть осторожным в том, как вы наследуете. Каждое существующее поле или метод в базовом классе должно иметь смысл в наследнике, особенно когда он приводится в качестве базового класса.
Интерфейсы полезны, когда вам нужно одинаково взаимодействовать с набором классов, но вы не можете предсказать или не заботиться об их фактической реализации. Взять, к примеру, что-то вроде интерфейса Collection. Господь знает только множество способов, которыми кто-то может решить реализовать коллекцию предметов. Связанный список? Список массивов? Стек? Очередь? Этот метод SearchAndReplace не имеет значения, ему просто нужно передать то, что он может вызвать Add, Remove и GetEnumerator.
источник
Я как бы просматриваю ответы на этот вопрос, чтобы увидеть, что говорят другие люди, но я скажу три вещи, которые я склонен об этом думать:
Люди вкладывают слишком много веры в интерфейсы и слишком мало веры в классы. Интерфейсы по существу очень разбавлены базовыми классами, и бывают случаи, когда использование чего-то более легкого может привести к таким вещам, как избыточный шаблонный код.
Нет ничего плохого в том, чтобы переопределить метод, вызвать
super.method()
его и позволить остальному его телу просто делать вещи, которые не мешают базовому классу. Это не нарушает принцип замещения Лискова .Как правило, быть таким жестким и догматичным в разработке программного обеспечения - плохая идея, даже если что-то обычно является лучшей практикой.
Поэтому я бы взял то, что было сказано, с крошкой соли.
источник
People put way too much faith into interfaces, and too little faith into classes.
Веселая. Я склонен думать, что все наоборот.Я хочу использовать языковой и семантический подход к этому. Интерфейс не существует для
DRY
. Хотя его можно использовать в качестве механизма централизации вместе с абстрактным классом, который его реализует, он не предназначен для DRY.Интерфейс - это контракт.
IAnimal
Интерфейс - это контракт «все или ничего» между аллигатором, кошкой, собакой и понятием животного. По сути, это говорит о том, что «если вы хотите быть животным, вы должны обладать всеми этими атрибутами и характеристиками, или вы больше не животное».Наследование с другой стороны является механизмом повторного использования. В этом случае, я думаю, хорошее семантическое мышление может помочь вам решить, какой метод должен пойти и куда. Например, хотя все животные могут спать и спать одинаково, я не буду использовать для этого базовый класс. Сначала я помещаю
Sleep()
метод вIAnimal
интерфейс как акцент (семантический) для того, что требуется, чтобы стать животным, затем я использую базовый абстрактный класс для кошек, собак и аллигаторов в качестве техники программирования, чтобы не повторяться.источник
Я не уверен, что это лучшее эмпирическое правило. Вы можете поместить
abstract
метод в базовый класс, и каждый класс может его реализовать. Поскольку каждыйmammal
должен есть, имеет смысл сделать это. Тогда каждыйmammal
может использовать свойeat
метод. Обычно я использую интерфейсы только для связи между объектами или как маркер, чтобы пометить класс как нечто. Я думаю, что чрезмерное использование интерфейсов приводит к небрежному коду.Я также согласен с @Panzercrisis, что эмпирическое правило должно быть просто общим руководством. Не то, что должно быть написано в камне. Несомненно, будут времена, когда это просто не работает.
источник
Интерфейсы являются типами. Они не обеспечивают реализацию. Они просто определяют контракт на обработку этого типа. Этот контракт должен выполняться любым классом, реализующим интерфейс.
Использование интерфейсов и абстрактных / базовых классов должно определяться проектными требованиями.
Один класс не требует интерфейса.
Если два класса являются взаимозаменяемыми внутри функции, они должны реализовать общий интерфейс. Любая функциональность, реализованная одинаково, может быть затем преобразована в абстрактный класс, реализующий интерфейс. Интерфейс может считаться избыточным в этот момент, если нет отличительной функциональности. Но тогда в чем разница между двумя классами?
Использование абстрактных классов в качестве типов обычно беспорядочно, поскольку добавляется больше функциональных возможностей и расширяется иерархия.
Пользуйся композицией, а не наследством - все это уходит.
источник