Интерфейсы на абстрактном классе

30

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

interface IFooWorker { void Work(); }

abstract class BaseWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker, IFooWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

DbWorker - это то, что получает интерфейс IFooWorker, потому что это инстанцируемая реализация интерфейса. Он полностью выполняет договор. Мой коллега предпочитает почти идентичные:

interface IFooWorker { void Work(); }

abstract class BaseWorker : IFooWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

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

Каковы плюсы и минусы его метода по сравнению с моим, и почему один должен использоваться поверх другого?

Брайан Бетчер
источник
Ваше предложение очень похоже на наследование алмазов , которое может привести к путанице в будущем.
Спойк
@Spoike: Как это увидеть?
Niing
@ У ссылки, которую я привел в комментарии, есть простая диаграмма классов и простое однострочное объяснение того, что это такое. это называется «алмазной» проблемой, потому что структура наследования в основном нарисована в форме ромба (т.е. ♦).
Спойк
@Spoike: почему этот вопрос вызовет наследование алмазов?
Ниин
1
@Niing: Наследование BaseWorker и IFooWorker с использованием одного и того же метода называется Workпроблемой наследования алмазов (в этом случае они оба выполняют контракт по Workметоду). В Java вам необходимо реализовать методы интерфейса, чтобы таким образом Workизбежать вопроса о том, какой метод должна использовать программа. Такие языки, как C ++, однако, не делают это однозначным.
Спойк

Ответы:

24

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

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

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

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

Карл Билефельдт
источник
1
+1 за указание на то, что просто потому, что BaseWorkerон не может быть создан напрямую, не означает, что данное BaseWorker myWorkerне реализуется IFooWorker.
Авнер Шахар-Каштан
2
Я не согласен с тем, что интерфейсы являются просто абстрактными классами. Абстрактные классы обычно определяют некоторые детали реализации (в противном случае был бы использован интерфейс). Если эти детали вам не нравятся, теперь вы должны изменить каждое использование базового класса на что-то другое. Интерфейсы неглубокие; они определяют взаимосвязь между зависимостями и зависимостями. Как таковая, тесная связь с любой деталью реализации больше не является проблемой. Если класс, унаследовавший интерфейс, вам не нравится, удалите его и вставьте в него что-то другое; Ваш код может заботиться меньше.
KeithS
5
Оба имеют свои преимущества, но обычно для зависимости всегда должен использоваться интерфейс , независимо от того, существует ли абстрактный класс, определяющий базовые реализации. Сочетание интерфейса и абстрактного класса дает лучшее из обоих миров; Интерфейс поддерживает взаимосвязь plug-and-socket мелкой поверхности, в то время как абстрактный класс предоставляет полезный общий код. Вы можете раздеть и заменить любой уровень этого под интерфейсом по желанию.
KeithS
Под «указателем» вы подразумеваете экземпляр объекта (на который будет ссылаться указатель)? Интерфейсы не являются классами, они являются типами и могут выступать в качестве неполной замены множественного наследования, а не замены, то есть они «включают поведение из нескольких источников в классе».
Самис
46

Я должен согласиться с вашим коллегой.

В обоих приведенных вами примерах BaseWorker определяет абстрактный метод Work (), что означает, что все подклассы способны выполнять контракт IFooWorker. В этом случае, я думаю, BaseWorker должен реализовать интерфейс, и эта реализация будет наследоваться его подклассами. Это избавит вас от необходимости явно указывать, что каждый подкласс действительно является IFooWorker (принцип DRY).

Если вы не определили Work () как метод BaseWorker, или если у IFooWorker были другие методы, которые подклассы BaseWorker не хотели бы или не нужны, то (очевидно) вам придется указать, какие подклассы действительно реализуют IFooWorker.

Мэтью Флинн
источник
11
+1 написал бы тоже самое. Извините, инста, но я думаю, что ваш коллега прав и в этом случае, если что-то гарантирует выполнение контракта, он должен наследовать этот контракт, независимо от того, выполняется ли гарантия интерфейсом, абстрактным классом или конкретным классом. Проще говоря: гарант должен унаследовать договор, который он гарантирует.
Джимми Хоффа
2
Я в целом согласен, но хотел бы отметить, что такие слова, как «base», «standard», «default», «generic» и т. Д., Являются запахами кода в имени класса. Если абстрактный базовый класс имеет почти то же имя, что и интерфейс, но с одним из этих слов ласки, это часто является признаком неправильной абстракции; интерфейсы определяют, что что-то делает , наследование типов определяет, что это такое . Если у вас есть IFooи a, BaseFooто это означает, что либо IFooэто не совсем детализированный интерфейс, либо BaseFooиспользуется наследование, когда композиция может быть лучшим выбором.
Aaronaught
@Aaronaught Хорошая мысль, хотя может случиться так, что действительно есть что-то, что все или большинство реализаций IFoo должны унаследовать (например, дескриптор сервиса или ваша реализация DAO).
Мэтью Флинн
2
Тогда не базовый класс можно назвать MyDaoFooили SqlFooили HibernateFooили независимо от того , чтобы указать , что это только один из возможных дерево классов , реализующих IFoo? Это даже больше пахнет для меня, если базовый класс связан с определенной библиотекой или платформой, и об этом нет упоминания в названии ...
Aaronaught
@ Aaronaught - Да. Я полностью согласен.
Мэтью Флинн
11

Я в целом согласен с вашим коллегой.

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

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

Модель вашего коллеги намного проще в использовании и замене. «Все BaseWorkers являются IFooWorkers» теперь является верным утверждением; Вы можете передать любой экземпляр BaseWorker любой зависимости IFooWorker, без проблем. Противоположное утверждение «Все IFooWorkers являются BaseWorkers» не соответствует действительности; это позволяет вам заменить BaseWorker на BetterBaseWorker, и ваш потребляющий код (который зависит от IFooWorker) не должен заметить разницу.

Keiths
источник
Мне нравится звучание этого ответа, но я не совсем слежу за последней частью. Вы предлагаете, чтобы BetterBaseWorker был независимым абстрактным классом, который реализует IFooWorker, так что мой гипотетический плагин мог бы просто сказать: «О, мальчик! Лучший базовый работник наконец-то вышел!» и поменять любые ссылки на BaseWorker на BetterBaseWorker и назвать это днем ​​(возможно, поиграть с новыми крутыми возвращаемыми значениями, которые позволят мне вырезать огромные куски моего избыточного кода и т. д.)?
Энтони
Более или менее да. Большим преимуществом является то, что вы уменьшите количество ссылок на BaseWorker, которые вместо этого должны будут измениться на BetterBaseWorkers; большая часть вашего кода не будет ссылаться на конкретный класс напрямую, вместо этого используется IFooWorker, поэтому при изменении того, что назначено этим зависимостям IFooWorker (свойствам классов, параметрам конструкторов или методов), код, использующий IFooWorker, не должен видеть никакой разницы в использовании.
KeithS
6

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

Возьмем классы платформы Java коллекции:

abstract class AbstractList
class LinkedList extends AbstractList implements Queue

Тот факт, что контракт очереди реализован LinkedList , не подтолкнул проблему в AbstractList .

В чем разница между этими двумя случаями? Цель BaseWorker всегда заключалась в том, чтобы (как сообщается по его имени и интерфейсу) реализовывать операции в IWorker . Назначение AbstractList и Queue расходятся, но потомок первого может реализовать последний контракт.

Михай Данила
источник
Это происходит в половине случаев. Он всегда предпочитает реализовывать интерфейс на базовом классе, а я всегда предпочитаю реализовывать его на конечном бетоне. Сценарий, который вы представили, случается часто и является частью причин, по которым интерфейсы на базе меня беспокоят.
Брайан Бетчер
1
Правильно, insta, и я рассматриваю использование интерфейсов таким образом как аспект чрезмерного использования наследования, которое мы наблюдаем во многих средах разработки. Как сказал GoF в своей книге шаблонов проектирования, предпочитайте композицию наследованию , и сохранение интерфейса вне базового класса - один из способов продвижения этого самого принципа.
Михай Данила
1
Я получаю предупреждение, но вы заметите, что абстрактный класс OP включает абстрактные методы, соответствующие интерфейсу: BaseWorker неявно является IFooWorker. Если сделать это явным, этот факт станет более полезным. Существует множество методов, включенных в Queue, которые, однако, не включены в AbstractList, и как таковой, AbstractList не является Queue, даже если его дочерние элементы могут быть. AbstractList и Queue являются ортогональными.
Мэтью Флинн
Спасибо за это понимание; Я согласен с тем, что случай с ФП попадает в эту категорию, но я почувствовал, что в поисках более глубокого правила произошла большая дискуссия, и я хотел поделиться своими наблюдениями о тенденции, о которой я читал (и даже имел себя на короткое время) чрезмерного использования наследования, возможно, потому что это один из инструментов в панели инструментов ООП.
Михай Данила
Черт, прошло шесть лет. :)
Михай Данила
2

Я хотел бы задать вопрос, что происходит при изменении IFooWorker, например при добавлении нового метода?

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

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

Скотт Уитлок
источник
1

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

Люди часто думают, что оба понятия очень похожи, на самом деле это общий вопрос для интервью (разница между ними есть ??)

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

В качестве примера, одним из распространенных применений этого является IDisposable. Абстрактный класс реализует метод Dispose IDisposable, что означает, что любая производная версия базового класса будет автоматически доступна. Затем вы можете играть с несколькими вариантами. Сделайте Dispose абстрактным, заставляя производные классы реализовывать его. Предоставьте виртуальную реализацию по умолчанию, чтобы они не делали ее или не делали ее ни виртуальной, ни абстрактной, и чтобы она вызывала виртуальные методы, называемые, например, BeforeDispose, AfterDispose, OnDispose или Disposing.

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

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

Ян
источник
1

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

Первый:

Есть два общих объяснения интерфейса:

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

  2. Интерфейсы - это абстрактные классы, которые ничего не делают / не могут делать. Они полезны, потому что вы можете реализовать более одного, в отличие от тех средних родительских классов. Например, я мог бы быть объектом класса BananaBread и наследоваться от BaseBread, но это не значит, что я не могу также реализовать интерфейсы IWithNuts и ITastesYummy. Я мог бы даже реализовать интерфейс IDoesTheDishes, потому что я не просто хлеб, понимаешь?

Есть два общих объяснения абстрактного класса:

  1. Знаете, абстрактный класс - это вещь, а не то, что она может сделать. Это похоже на суть, а на самом деле совсем не реально. Погоди, это поможет. Лодка - это абстрактный класс, но сексуальная Playboy Yacht будет подклассом BaseBoat.

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

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

Во-вторых:

На SO кто-то задал более простую версию этого вопроса, классическую: «Зачем использовать интерфейсы? Какая разница? Что мне не хватает?» И в одном из ответов в качестве простого примера использовался пилот ВВС. Он не совсем приземлился, но вызвал несколько замечательных комментариев, в одном из которых упоминался интерфейс IFlyable с такими методами, как takeOff, pilotEject и т. Д. И это действительно показалось мне реальным примером того, почему интерфейсы не просто полезны, но важно. Интерфейс делает объект / класс интуитивно понятным или, по крайней мере, дает ощущение, что это так. Интерфейс не для выгоды объекта или данных, но для чего-то, что должно взаимодействовать с этим объектом. Классический Fruit-> Apple-> Fuji или Shape-> Triangle-> Равносторонние примеры наследования являются отличной моделью для таксономического понимания данного объекта на основе его потомков. Он информирует потребителя и процессоров о его общих качествах, поведении, о том, является ли объект группой вещей, будет ли нарушать работу вашей системы, если вы поместите его в неправильное место, или описывает сенсорные данные, соединитель с конкретным хранилищем данных или финансовые данные, необходимые для расчета заработной платы.

Определенный объект Plane может иметь или не иметь метод для экстренной посадки, но я разозлюсь, если предположу, что его аварийная зона, как и любой другой летучий объект, просыпается только в DumbPlane и узнает, что разработчики пошли с quickLand потому что они думали, что это было все то же самое. Точно так же, как я был бы расстроен, если бы у каждого производителя винтов была своя собственная интерпретация правильной силы или если у моего телевизора не было кнопки увеличения громкости над кнопкой уменьшения громкости.

Абстрактные классы - это модель, которая устанавливает, что любой потомственный объект должен квалифицировать как этот класс. Если вы не обладаете всеми качествами утки, то не имеет значения, что у вас реализован интерфейс IQuack, ваш просто странный пингвин. Интерфейсы - это то, что имеет смысл, даже если вы не можете быть уверены ни в чем другом. Джефф Голдблюм и Starbuck оба могли управлять космическими кораблями пришельцев, потому что интерфейс был надежно подобен.

В третьих:

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

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

Энтони
источник
0

Есть еще один аспект, почему DbWorkerследует реализовать IFooWorker, как вы предлагаете.

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

Фредерик Краутвальд
источник
-1

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

Interface: a contract that each class must fulfill if they are to implement that interface
Abstract class: a general class (i.e vehicle is an abstract class, while porche911 is not)

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

Теперь, DbWorkerудовлетворяет ли IFooWorkerинтерфейс, но если есть определенный способ, которым этот класс делает work(), тогда он действительно должен перегрузить определение, унаследованное от BaseWorker. Причина, по которой он не должен реализовывать интерфейс IFooWorkerнапрямую, заключается в том, что в этом нет ничего особенного DbWorker. Если бы вы делали это каждый раз, когда реализовывали классы, подобные тем, которые DbWorkerвы использовали, вы бы нарушали DRY .

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

Арнаб Датта
источник
-3

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

Мартин Маат
источник
3
Это явная ложь. Интерфейс - это контракт, по которому система должна вести себя независимо от реализации. Унаследованный базовый класс является одной из многих возможных реализаций этого интерфейса. В достаточно крупномасштабной системе будет несколько базовых классов, удовлетворяющих различным интерфейсам (обычно закрытым дженериками), что и было сделано в этой настройке, и поэтому было полезно их разделить.
Брайан Бетчер
Это действительно плохой пример с точки зрения ООП. Поскольку вы говорите, что «базовый класс - это всего лишь одна реализация» (это должно быть или не было бы смысла для интерфейса), вы говорите, что будут нерабочие классы, реализующие интерфейс IFooWorker. Тогда у вас будет базовый класс, который является специализированным специалистом по работе с людьми (мы должны предположить, что там могут быть и бармены), и у вас будут другие сотрудники, которые даже не являются базовыми работниками! Это назад. Более чем одним способом.
Мартин Маат
1
Возможно, вы застряли на слове «рабочий» в интерфейсе. А как насчет "источника данных"? Если у нас есть IDatasource <T>, мы можем тривиально иметь SqlDatasource <T>, RestDatasource <T>, MongoDatasource <T>. Бизнес решает, какие замыкания универсального интерфейса идут на закрывающие реализации абстрактных источников данных.
Брайан Бетчер
Может иметь смысл использовать наследование только ради DRY и помещать некоторую общую реализацию в базовый класс. Но вы бы не совмещали переопределенные методы с какими-либо интерфейсными методами, базовый класс содержал бы общую логику поддержки. Если бы они выстроились в линию, это показало бы, что наследование решает вашу проблему просто отлично, и интерфейс не будет бесполезным. Вы используете интерфейс в тех случаях, когда наследование не решает вашу проблему, потому что общие характеристики, которые вы хотите смоделировать, не соответствуют дереву классов singke. И да, формулировка в примере делает его еще более неправильным.
Мартин Маат