У меня и моего коллеги разные мнения о взаимоотношениях между базовыми классами и интерфейсами. Я полагаю, что класс не должен реализовывать интерфейс, если только этот класс не может использоваться, когда требуется реализация интерфейса. Другими словами, мне нравится видеть такой код:
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()));
}
}
Где базовый класс получает интерфейс, и в силу этого все наследники базового класса также относятся к этому интерфейсу. Это меня беспокоит, но я не могу придумать конкретные причины, по которым за пределами «базовый класс не может выступать в качестве реализации интерфейса».
Каковы плюсы и минусы его метода по сравнению с моим, и почему один должен использоваться поверх другого?
источник
Work
проблемой наследования алмазов (в этом случае они оба выполняют контракт поWork
методу). В Java вам необходимо реализовать методы интерфейса, чтобы таким образомWork
избежать вопроса о том, какой метод должна использовать программа. Такие языки, как C ++, однако, не делают это однозначным.Ответы:
BaseWorker
выполняет это требование. То, что вы не можете напрямую создать экземплярBaseWorker
объекта, не означает, что у вас не может бытьBaseWorker
указателя, который выполняет контракт. Действительно, в этом и заключается весь смысл абстрактных классов.Кроме того, по простому примеру, который вы опубликовали, трудно сказать, но часть проблемы может заключаться в том, что интерфейс и абстрактный класс являются избыточными. Если у вас не реализованы другие классы,
IFooWorker
которые не являются производнымиBaseWorker
, вам вообще не нужен интерфейс. Интерфейсы - это просто абстрактные классы с некоторыми дополнительными ограничениями, которые упрощают множественное наследование.Опять же, трудно сказать из упрощенного примера, использование защищенного метода, явная ссылка на базу из производного класса и отсутствие однозначного места для объявления реализации интерфейса - это предупреждающие признаки того, что вы ненадлежащим образом используете наследование вместо состав. Без этого наследства весь ваш вопрос становится спорным.
источник
BaseWorker
он не может быть создан напрямую, не означает, что данноеBaseWorker myWorker
не реализуетсяIFooWorker
.Я должен согласиться с вашим коллегой.
В обоих приведенных вами примерах BaseWorker определяет абстрактный метод Work (), что означает, что все подклассы способны выполнять контракт IFooWorker. В этом случае, я думаю, BaseWorker должен реализовать интерфейс, и эта реализация будет наследоваться его подклассами. Это избавит вас от необходимости явно указывать, что каждый подкласс действительно является IFooWorker (принцип DRY).
Если вы не определили Work () как метод BaseWorker, или если у IFooWorker были другие методы, которые подклассы BaseWorker не хотели бы или не нужны, то (очевидно) вам придется указать, какие подклассы действительно реализуют IFooWorker.
источник
IFoo
и a,BaseFoo
то это означает, что либоIFoo
это не совсем детализированный интерфейс, либоBaseFoo
используется наследование, когда композиция может быть лучшим выбором.MyDaoFoo
илиSqlFoo
илиHibernateFoo
или независимо от того , чтобы указать , что это только один из возможных дерево классов , реализующихIFoo
? Это даже больше пахнет для меня, если базовый класс связан с определенной библиотекой или платформой, и об этом нет упоминания в названии ...Я в целом согласен с вашим коллегой.
Давайте возьмем вашу модель: интерфейс реализуется только дочерними классами, хотя базовый класс также обеспечивает использование методов IFooWorker. Во-первых, это избыточно; независимо от того, реализует ли дочерний класс интерфейс или нет, они должны переопределять открытые методы BaseWorker, а также любая реализация IFooWorker должна предоставлять функциональность независимо от того, получают ли они какую-либо помощь от BaseWorker или нет.
Это дополнительно делает вашу иерархию неоднозначной; «Все IFooWorkers являются BaseWorkers» не обязательно являются верным утверждением, равно как и «Все BaseWorkers не являются IFooWorkers». Таким образом, если вы хотите определить переменную экземпляра, параметр или тип возвращаемого значения, которые могут быть любой реализацией IFooWorker или BaseWorker (используя преимущества общей предоставляемой функциональности, которая является одной из причин наследования в первую очередь), то ни один из они гарантированно будут всеобъемлющими; некоторые BaseWorkers не могут быть назначены переменной типа IFooWorker, и наоборот.
Модель вашего коллеги намного проще в использовании и замене. «Все BaseWorkers являются IFooWorkers» теперь является верным утверждением; Вы можете передать любой экземпляр BaseWorker любой зависимости IFooWorker, без проблем. Противоположное утверждение «Все IFooWorkers являются BaseWorkers» не соответствует действительности; это позволяет вам заменить BaseWorker на BetterBaseWorker, и ваш потребляющий код (который зависит от IFooWorker) не должен заметить разницу.
источник
Мне нужно добавить что-то вроде предупреждения к этим ответам. Соединение базового класса с интерфейсом создает силу в структуре этого класса. В вашем базовом примере нет ничего сложного в том, что эти два элемента должны быть связаны, но это может не соответствовать действительности во всех случаях.
Возьмем классы платформы Java коллекции:
Тот факт, что контракт очереди реализован LinkedList , не подтолкнул проблему в AbstractList .
В чем разница между этими двумя случаями? Цель BaseWorker всегда заключалась в том, чтобы (как сообщается по его имени и интерфейсу) реализовывать операции в IWorker . Назначение AbstractList и Queue расходятся, но потомок первого может реализовать последний контракт.
источник
Я хотел бы задать вопрос, что происходит при изменении
IFooWorker
, например при добавлении нового метода?Если
BaseWorker
реализует интерфейс, по умолчанию он должен будет объявить новый метод, даже если он абстрактный. С другой стороны, если он не реализует интерфейс, вы получите ошибки компиляции только в производных классах.По этой причине я бы заставил базовый класс реализовывать интерфейс , поскольку мог бы реализовать все функциональные возможности для нового метода в базовом классе, не касаясь производных классов.
источник
Сначала подумайте, что такое абстрактный базовый класс и что такое интерфейс. Подумайте, когда вы будете использовать один или другой, а когда нет.
Люди часто думают, что оба понятия очень похожи, на самом деле это общий вопрос для интервью (разница между ними есть ??)
Таким образом, абстрактный базовый класс дает то, что интерфейсы не могут, что является реализацией методов по умолчанию. (Есть другие вещи, в C # вы можете использовать статические методы в абстрактном классе, а не в интерфейсе, например).
В качестве примера, одним из распространенных применений этого является IDisposable. Абстрактный класс реализует метод Dispose IDisposable, что означает, что любая производная версия базового класса будет автоматически доступна. Затем вы можете играть с несколькими вариантами. Сделайте Dispose абстрактным, заставляя производные классы реализовывать его. Предоставьте виртуальную реализацию по умолчанию, чтобы они не делали ее или не делали ее ни виртуальной, ни абстрактной, и чтобы она вызывала виртуальные методы, называемые, например, BeforeDispose, AfterDispose, OnDispose или Disposing.
Таким образом, каждый раз, когда все производные классы должны поддерживать интерфейс, он переходит на базовый класс. Если этот интерфейс нужен только одному или нескольким пользователям, он перейдет в производный класс.
Это все на самом деле грубое упрощение. Другая альтернатива - иметь производные классы, которые даже не реализуют интерфейсы, а предоставляют их через шаблон адаптера. Пример того, что я видел недавно, был в IObjectContextAdapter.
источник
Мне еще далеко до полного понимания различия между абстрактным классом и интерфейсом. Каждый раз, когда мне кажется, что я понимаю основные понятия, я смотрю на stackexchange и возвращаюсь на два шага назад. Но несколько мыслей по теме и вопросу ОП:
Первый:
Есть два общих объяснения интерфейса:
Интерфейс - это список методов и свойств, которые может реализовать любой класс, и, реализуя интерфейс, класс гарантирует, что эти методы (и их сигнатуры) и эти свойства (и их типы) будут доступны при «взаимодействии» с этим классом или объект этого класса. Интерфейс - это контракт.
Интерфейсы - это абстрактные классы, которые ничего не делают / не могут делать. Они полезны, потому что вы можете реализовать более одного, в отличие от тех средних родительских классов. Например, я мог бы быть объектом класса BananaBread и наследоваться от BaseBread, но это не значит, что я не могу также реализовать интерфейсы IWithNuts и ITastesYummy. Я мог бы даже реализовать интерфейс IDoesTheDishes, потому что я не просто хлеб, понимаешь?
Есть два общих объяснения абстрактного класса:
Знаете, абстрактный класс - это вещь, а не то, что она может сделать. Это похоже на суть, а на самом деле совсем не реально. Погоди, это поможет. Лодка - это абстрактный класс, но сексуальная Playboy Yacht будет подклассом BaseBoat.
Я прочитал книгу об абстрактных классах, и, возможно, вам следует прочитать эту книгу, потому что вы, вероятно, не поняли ее и сделаете это неправильно, если не читали эту книгу.
Кстати, книга Ситерс всегда кажется впечатляющей, даже если я все равно уйду в замешательстве.
Во-вторых:
На SO кто-то задал более простую версию этого вопроса, классическую: «Зачем использовать интерфейсы? Какая разница? Что мне не хватает?» И в одном из ответов в качестве простого примера использовался пилот ВВС. Он не совсем приземлился, но вызвал несколько замечательных комментариев, в одном из которых упоминался интерфейс IFlyable с такими методами, как takeOff, pilotEject и т. Д. И это действительно показалось мне реальным примером того, почему интерфейсы не просто полезны, но важно. Интерфейс делает объект / класс интуитивно понятным или, по крайней мере, дает ощущение, что это так. Интерфейс не для выгоды объекта или данных, но для чего-то, что должно взаимодействовать с этим объектом. Классический Fruit-> Apple-> Fuji или Shape-> Triangle-> Равносторонние примеры наследования являются отличной моделью для таксономического понимания данного объекта на основе его потомков. Он информирует потребителя и процессоров о его общих качествах, поведении, о том, является ли объект группой вещей, будет ли нарушать работу вашей системы, если вы поместите его в неправильное место, или описывает сенсорные данные, соединитель с конкретным хранилищем данных или финансовые данные, необходимые для расчета заработной платы.
Определенный объект Plane может иметь или не иметь метод для экстренной посадки, но я разозлюсь, если предположу, что его аварийная зона, как и любой другой летучий объект, просыпается только в DumbPlane и узнает, что разработчики пошли с quickLand потому что они думали, что это было все то же самое. Точно так же, как я был бы расстроен, если бы у каждого производителя винтов была своя собственная интерпретация правильной силы или если у моего телевизора не было кнопки увеличения громкости над кнопкой уменьшения громкости.
Абстрактные классы - это модель, которая устанавливает, что любой потомственный объект должен квалифицировать как этот класс. Если вы не обладаете всеми качествами утки, то не имеет значения, что у вас реализован интерфейс IQuack, ваш просто странный пингвин. Интерфейсы - это то, что имеет смысл, даже если вы не можете быть уверены ни в чем другом. Джефф Голдблюм и Starbuck оба могли управлять космическими кораблями пришельцев, потому что интерфейс был надежно подобен.
В третьих:
Я согласен с вашим коллегой, потому что иногда вам нужно применять определенные методы с самого начала. Если вы создаете Active Record ORM, ему нужен метод сохранения. Это не до подкласса, который может быть создан. И если интерфейс ICRUD является достаточно переносимым, чтобы не связываться исключительно с одним абстрактным классом, он может быть реализован другими классами, чтобы сделать их надежными и интуитивно понятными для любого, кто уже знаком с любым из классов-потомков этого абстрактного класса.
С другой стороны, ранее был отличный пример того, когда нельзя переходить к привязке интерфейса к абстрактному классу, поскольку не все типы списков будут (или должны) реализовывать интерфейс очереди. Вы сказали, что этот сценарий происходит в половине случаев, а это значит, что вы и ваш коллега оба не правы в половине случаев, и поэтому лучше всего спорить, обсуждать, обдумывать, а если они окажутся правильными, признать и принять связь , Но не становитесь разработчиком, который следует философии, даже если она не самая лучшая для работы.
источник
Есть еще один аспект, почему
DbWorker
следует реализоватьIFooWorker
, как вы предлагаете.В случае стиля вашего коллеги, если по какой-либо причине происходит более поздний рефакторинг и
DbWorker
считается, что он не расширяет абстрактныйBaseWorker
класс, онDbWorker
теряетIFooWorker
интерфейс. Это может, но не обязательно, повлиять на клиентов, потребляющих его, если они ожидаютIFooWorker
интерфейс.источник
Для начала я хотел бы по-разному определять интерфейсы и абстрактные классы:
В вашем случае все работники внедряют,
work()
потому что это то, чего требует контракт. Поэтому ваш базовый классBaseworker
должен реализовывать этот метод либо явно, либо как виртуальная функция. Я бы посоветовал вам включить этот метод в ваш базовый класс из-за простого факта, который нужен всем работникамwork()
. Поэтому логично, что ваш базовый класс (даже если вы не можете создать указатель этого типа) включает его в качестве функции.Теперь,
DbWorker
удовлетворяет лиIFooWorker
интерфейс, но если есть определенный способ, которым этот класс делаетwork()
, тогда он действительно должен перегрузить определение, унаследованное отBaseWorker
. Причина, по которой он не должен реализовывать интерфейсIFooWorker
напрямую, заключается в том, что в этом нет ничего особенногоDbWorker
. Если бы вы делали это каждый раз, когда реализовывали классы, подобные тем, которыеDbWorker
вы использовали, вы бы нарушали DRY .Если два класса реализуют одну и ту же функцию по-разному, вы можете начать искать самый распространенный суперкласс. Большую часть времени вы найдете один. Если нет, то продолжайте искать или сдавайтесь и признайте, что у них недостаточно общего для создания базового класса.
источник
Ничего себе, все эти ответы и ни один из них не указывает на то, что интерфейс в этом примере не имеет никакого значения. Вы не используете наследование и интерфейс для одного и того же метода, это бессмысленно. Вы используете один или другой. На этом форуме есть множество вопросов с ответами, объясняющими преимущества каждого из них и сенарио, которые требуют того или другого.
источник