Как стать автором
Обновить

Комментарии 26

  1. Почти все приведённые аргументы относятся к базовым простым и базовым абстрактным классам.

  2. Не показано самого главного - когда применять базовые классы, а когда интерфейсы.

Про инверсию зависимостей я тоже не увидел

И инверсия тоже прекрасно делается через классы.

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

Объет может создаваться как угодно всегда.

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

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

А если один класс должен реализовывать несколько разных интерфейсов?

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

В моем комментарии не столько про саму инверсию было, сколько про то, что не все делается классами и наследованием.

Это значит что у вас проблемы с архитектурой

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

Это значит что у вас проблемы с архитектурой.

Нет, не значит. Это тотально распростаненный случай, когда разные компоненты требуют чтобы объект который они принимают имел нужный им интерфейс. Можно открыть библиотеку NET, и увидеть что почти все его классы имеют более одного интерфейса. У банального Int32 их пять штук. Через один интерфейс объект может сравниваться для сортировки, через второй считать хеш, через другие итерироваться, клонироваться, сериализоваться, и т.д. и т.п, всяких I*able интерфейсов десятки.

Более того, это и есть Interface Segregation Principle из SOLID, т.е. идиоматическое ООП.

А interface segregation из solid уже выкинули?

Именно это и является единственным (пока) очевидным случаем.

Для internal можно закрыть глаза и делать как душа пожелает - всегда можно отрефакторить. А вот когда из публичного API торчат интерфейсы, там где надо было бы абстрактные классы c protected internal конструкторами, вот тогда возникают проблемы. Любое изменение будет либо ломающим, либо добавлением интерфейса вида ICoolStufExtended_Version_2.

Но, видимо, "ведущие эксперты по цифровым навыкам" не готовы делиться таким секретами.

С другой стороны, абстрактный класс с protected internal конструктором резко снижает возможности расширения.

Именно. Поэтому всему свое место.

интерфейс по своей природе и есть абстрактный класс (в с++ из которого он пошёл развиваться далее - сначала в Java - не уверен в каком ЯП раньше, но в одном из императивных языков появился интерфейс как замена абстрактному классу), затем и в C#.

Сейчас interface в C# имеет неоспоримое отличительное преимущество по сравнению с abstract class - и меняно:

  1. Множественное "наследование" вернее реализацию - "производный" класс может наследоваться от одного базового класса и при этом реализовывать (поддерживать) любое количество интерфейсов (конечно для C++ это не было бы ограничением для абстрактных классов - но это было бы плохим дизайном)

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

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

  4. Так же это позволяет решать проблему совпадения имён членов(сигнатуры вызова) - один и тот же класс может поддерживать кучу интерфейсов с одинаковыми именами членов - но реализуемых отдельным независимыми алгоритмами - доступ к нужной реализации одной и той же сигнатуры вызова члена будет определяться только после приведения к нужному интерфейсу - эдакая квинтэссенция множественного полиморфизма ООП.

  5. Интерфейсы в C# могут иметь реализацию членов по умолчанию (для многих это спорная фишка) - очень мощный механизм как увеличения повторного использования кода - так и повышения уровня абстракции этого кода (особенно в сочетании к дженериками). "Множественное" наследование интерфейсов возносит эти возможности до недостижимых для абстрактных классов высот с сохранением стабильности и надёжности алгоритмов. В том числе при последующем развитии API.

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

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

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

    Но это оснащение так же должно включать в себя и части для проведения самотестирования - чем выше уровень абстракции - тем обязательнее должны быть юнит-тесты!

Добавлю ещё общее преимущество как для интерфейсов так и для абстрактных классов.

Интерфейс (или абстрактный класс при поддержке множественного наследоания) - далее это уточнение опускаю) может быть удобным типом передачи/приёме значения при абстрагировании в программировании. Можно объявить где в одной библиотеке (к примеру) какой-то Интерфейс - а в различных других библиотеках объявить классы, поддерживающие этот Интерфейс. Причём о друг друге эти классы ничего не знают (просто импортируют и реализуют общий Интерфейс). А потом в третьей библиотеке реализованы какие-то функции (не важно где расположенные, в классах или вне), которые принимают и ли возвращают тип такого интерфейса - они тоже ничего не знают созданных классах в других библиотеках и могут даже иметь свои - тоже поддерживающие это Интерфейс - но все они смогут работать с любыми объектами классов, реализующих этот Интерфейс - не просто принимать их в качестве аргументов, но и обращаться к доступным членам (имеющимся в этом Интерфейсе).

Функция так же может просто возвращать Интерфейс - создавая (или получая откуда-либо ещё) экземпляр некоторого класса - и вызывающий код не будет знать что это за класс - для него доступен просто тип Интерфейса - с ним он и будет работать!

И это одно из самых главных преимуществ Интерфейсного подхода (или абстрактных классов с множественным наследованием). Очень активно это получило развитие в технологии COM/OLE - когда разные приложения/библиотеки стали динамически взаимодействовать друг с другом - ничего не знаю о своих метаданных (это было ещё задолго до рефлексии .NET и даже до Java с её ущербной структурой метаданных, и даже до Delphi c её продвинутой структурой метаданных RTTI).

Не заменимы интерфейсы и при организации удалённых вызовов - начиная от устаревшей технологии DCOM - и до более современных SOAP и REST протоколов.

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

Для эффективного решения этой проблемы нужна очередная революция в программировании. Скорее всего уже вместе с переходом на декларативно-императивное программирование и 5-тое поколение ЯП (с развитием уже АОП как наследника ООП)

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

Написал сам несколько приложений, без всяких ООП, все вроде работает. Пытался перенести на понимание ООП- не получилось (((

Если нет большого опыта написания приложений в парадигме ООП - переносить на неё уже готовое процедурное или функциональное боле менее приложение будет крайне сложно - это всегда плохая идея. Для небольших приложений ООП то и не особо то и нужен - поэтому в C#10 стало возможно проектировать приложения без единого класса.

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

И для применения ООП требуется некая ломка сознания. Вникать в ООП нужно на практических примерах - либо реализуя их по книжкам либо по учебным курсам либо копаясь и дорабатывая готовые программы и библиотеки.

ООП сложнее в освоении чем просто процедурное программирование и сложнее в использовании - но зато в итоге потом получаются более простые, надёжные и легко дорабатываемые приложения (ну если ООП грамотно использован, конечно).

Когда-то давно я изучал исходные коды игры Doom - она написана была на чистом Си - и вот там ООП просто напрашивался на каждом шагу!

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

Вот где ооп напрашивался на каждом шагу - https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition (сарказм, если что). А в DOOM ооп вообще не нужен был. Потому что, внезапно, в DOOM решали задачу по быстродействию, а не красивого, понятного каждой домохозяйке (еще и на тот момент closed-source) кода. DOOM писался в такое время, что любой оверхед на вызов виртуальной функции, или неудачно запакованные данные могли бы стать фатальными недостатками. И наоборот, хитрая и не понятная для свидетелей ООП фигня давала очень хороший и желанный результат.

пс: ломка сознания для ООП - это звучит страшно. Ломать сознание надо для того что бы натянуть ООП везде и всегда? Мне всегда казалось что ООП это инструмент, а не стиль жизни, и для правильного применения ООП нужна не ломка сознания, а: инженерное образование; нормальные, структурированные знания; последовательность. Всегда думал, что обладая таким набором качеств, специалист будет уверенно применять и ООП, и ФП, и спокойно структурировать свой код(при надобности) и на чистом ASM или С, но я могу и ошибаться...

Про Doom я с Вами согласен - я привёл пример того, где явно по архитектуре ООП напрашивался. Но в те времена да - производительности была превыше всего - а оптимизирующие компиляторы только разрабатывались!

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

То же функциональное программирование - тоже требует определённой ломки сознания! Любая новая и не привычная парадигма требует ломки сознания!

В C# я, безусловно, лох. Наверное именно по этому я так и не понял,- что же автор хотел нам рассказать.

По поводу пункта "Польза от интерфейса № 1". Изменили классы и перевели связи на интерфейсы. Красиво получилось, я тоже люблю, когда красивая картинка получается. Но есть одно "но"! Раньше Менеджер мог оценивать только Служащего, а теперь может оценивать и другого Менеджера. Изменились бизнес-правила во время рефакторинга, так не должно быть, это не фича, это баг.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий