Комментарии 26
Почти все приведённые аргументы относятся к базовым простым и базовым абстрактным классам.
Не показано самого главного - когда применять базовые классы, а когда интерфейсы.
Про инверсию зависимостей я тоже не увидел
И инверсия тоже прекрасно делается через классы.
Подход через интерфейсы все же более правильный и удобный. Например в случае чего можно на определенную реализацию навесить прокси, что через классы не всегда получится сделать (реализация может быть запечатана для наследования, нельзя перегрузить методы, либо объект создается через фабрику)
А если один класс должен реализовывать несколько разных интерфейсов?
P.S. Инверсия должна делаться интерфейсами, имхо
В моем комментарии не столько про саму инверсию было, сколько про то, что не все делается классами и наследованием.
Это значит что у вас проблемы с архитектурой
Не обязательно.
Например у DTO-шек вполне может быть много интерфейсов, что удобно для написания обобщённых linq-запросов.
Это значит что у вас проблемы с архитектурой.
Нет, не значит. Это тотально распростаненный случай, когда разные компоненты требуют чтобы объект который они принимают имел нужный им интерфейс. Можно открыть библиотеку NET, и увидеть что почти все его классы имеют более одного интерфейса. У банального Int32 их пять штук. Через один интерфейс объект может сравниваться для сортировки, через второй считать хеш, через другие итерироваться, клонироваться, сериализоваться, и т.д. и т.п, всяких I*able интерфейсов десятки.
Более того, это и есть Interface Segregation Principle из SOLID, т.е. идиоматическое ООП.
А interface segregation из solid уже выкинули?
Именно это и является единственным (пока) очевидным случаем.
Для internal можно закрыть глаза и делать как душа пожелает - всегда можно отрефакторить. А вот когда из публичного API торчат интерфейсы, там где надо было бы абстрактные классы c protected internal конструкторами, вот тогда возникают проблемы. Любое изменение будет либо ломающим, либо добавлением интерфейса вида ICoolStufExtended_Version_2.
Но, видимо, "ведущие эксперты по цифровым навыкам" не готовы делиться таким секретами.
интерфейс по своей природе и есть абстрактный класс (в с++ из которого он пошёл развиваться далее - сначала в Java - не уверен в каком ЯП раньше, но в одном из императивных языков появился интерфейс как замена абстрактному классу), затем и в C#.
Сейчас interface в C# имеет неоспоримое отличительное преимущество по сравнению с abstract class - и меняно:
Множественное "наследование" вернее реализацию - "производный" класс может наследоваться от одного базового класса и при этом реализовывать (поддерживать) любое количество интерфейсов (конечно для C++ это не было бы ограничением для абстрактных классов - но это было бы плохим дизайном)
Поддерживая несколько интерфейсов экземпляр такого класса можно далее проверять на поддержку этих интерфейсов - любого из них - и переходить к его типу для осуществления вызовов, или просто использовать интерфейсы как маркера ветвления, из области метаданных, а не из данных.
Классы могут скрыто поддерживать интерфейсы - когда их API данных интерфейсов снаружи не будет виден или виден под другим API. Скрытый API за интерфейсом позволяет не перегружать класс лишним API - доступ к которому будет только через приведение к интерфейсу, а если тип интерфейса будет вне зоны видимости - то вообще этот API будет скрыть в этой зоне алгоритма и не будет доступен к вызову. Не уверен, что такое можно сделать на абстрактных классах даже в С++ (в C# то уж точно не выйдет).
Так же это позволяет решать проблему совпадения имён членов(сигнатуры вызова) - один и тот же класс может поддерживать кучу интерфейсов с одинаковыми именами членов - но реализуемых отдельным независимыми алгоритмами - доступ к нужной реализации одной и той же сигнатуры вызова члена будет определяться только после приведения к нужному интерфейсу - эдакая квинтэссенция множественного полиморфизма ООП.
Интерфейсы в C# могут иметь реализацию членов по умолчанию (для многих это спорная фишка) - очень мощный механизм как увеличения повторного использования кода - так и повышения уровня абстракции этого кода (особенно в сочетании к дженериками). "Множественное" наследование интерфейсов возносит эти возможности до недостижимых для абстрактных классов высот с сохранением стабильности и надёжности алгоритмов. В том числе при последующем развитии API.
Интерфейсы - отличный способ избегать конфликтов имён членов - в т.ч. при "множественном" наследовании (реализации) интерфейсов классами, в т.ч. с реализациями по умолчанию и последующим добавлением в интерфейсы новых членов. /то один самых главных недостатков абстрактных классов и реального множественного наследования оных.
Но, хочу так же сказать - что Интерфейсы не панацея и не манна небесная. В 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". Изменили классы и перевели связи на интерфейсы. Красиво получилось, я тоже люблю, когда красивая картинка получается. Но есть одно "но"! Раньше Менеджер мог оценивать только Служащего, а теперь может оценивать и другого Менеджера. Изменились бизнес-правила во время рефакторинга, так не должно быть, это не фича, это баг.
Интерфейсы в C#: зачем они нужны?