Главная

Почему не стоит использовать интерфейсы.

Возможно, это непопулярное мнение, но игнорирование его может создать большие проблемы в будущем.

 

3+

Интерфейсы не настолько идеальны, как вы думаете. Давайте поясню почему.

Интерфейсы 101

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

Цель интерфейса — предоставить языку программирования некоторую степень развязки.
Например, если у вас есть класс, который должен читать внешние данные, он может быть таким:
IdomainObjectDataProvider
Вместо такого:
SqlServerDomainObjectDataProvider

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

Что не так с интерфейсами?

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

Навигация

Прежде всего, использование интерфейсов затрудняет навигацию в коде. Если я нахожусь в среде разработки, скорее всего она будет поддерживать клавиши управления или сочетания клавиш для перехода к определению типа. С конкретными типами навигация приведет вас непосредственно к реализации метода, который вас больше всего интересует. А в случае интерфейсов вы перейдете к определению интерфейса этого метода. Да, возможно это и не такая весомая проблема, но если вы находитесь «в потоке», то это ужасно раздражает. Вы должны сначала понять, что вам нужно, а затем еще и выяснить, с каким конкретным типом (типами) вы действительно работаете, дабы найти их соответствующее определение. В итоге вы платите за это своей производительностью. Особенно ярко эта проблема выражена в переполненных интерфейсами средах.

Обфускация кода

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

Допустим, у нас есть только один тип IEmalSender в приложении. Если все, что я вижу при навигации по коду, — это ссылка на IEmailSender, я могу потерять представление о том, с каким отправителем я на самом деле работаю в рабочей среде, и о некоторых особенностях его реализации. Некоторые могут сказать, что это хорошо, и они правы – в некоторой степени – но проблема возникает тогда, когда начинаешь смотреть на это с точки зрения абстракции. Тогда увидеть конкретный сценарий развертывания становится очень трудно.

Архитектурный цемент

Мне нравится воспринимать интерфейсы как своего рода «архитектурный цемент» в разработке программного обеспечения.

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

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

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

Принцип разделения интерфейса

Другая важная проблема — это нарушение принципа разделения интерфейса (ISP – interface segregation principle). ISP, в свою очередь, является частью SOLID принципов, которые являются пятью фундаментальными принципами для создания поддерживаемого программного обеспечения.

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

Это довольно просто и легко сделать, поэтому путь наименьшего сопротивления ведет к гигантским интерфейсам, таким как IUserRepository. Вместо этого следовало бы использовать более мелкие интерфейсы, такие как IUserValidator и IUserCreator.

Большие интерфейсы подвержены ряду проблем:
a) Они часто демонстрируют проблемы, перечисленные в предыдущих разделах.
b) Они затрудняют создание новых реализаций из-за количества методов, которые являются частью интерфейса.
c) Они, как правило, являются единственной конкретной реализацией этого интерфейса.
d) Они способствуют созданию классов, которые не придерживаются принципа единой ответственности с англ. SRP – single responsibility principle (часть SOLID).
В общем, большие интерфейсы — это плохая идея, которая в долгосрочной перспективе может привести к проблемам с обслуживанием.

Наследование против интерфейсов

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

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

В связи с этим я топлю за то, чтобы каждый раз, когда вы задумываетесь о добавлении нового интерфейса, вы рассматривали возможность внедрения или использования уже существующего базового класса.
Преимущества использования базовых классов:
• Переход к базовому классу — это фактически переход к конкретной реализации или реализации по умолчанию соответствующего метода.
• Базовые классы обеспечивают некую степень повторного использования / совместного использования кода, которая невозможна в случае интерфейсов.
• Базовые классы немного легче реорганизовать, чем интерфейсы.
Конечно, следует учитывать и недостатки:
• Код вашего базового класса будет присутствовать в любом производном классе, если не сделать переопределение. Это, в свою очередь, может значительно ограничивать реализацию.
• У вас может быть недостаточно кода (вы владеете не всем кодом проекта), чтобы сделать базовые классы качественными, либо зависимости делают это невозможным.
• Это может привести к слишком большой «глубине наследования», если в вашей иерархии классов уже есть наследование.

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

Заключение

Ваши предпочтения должны соответствовать вашим потребностям. Я просто хочу, чтобы вы не делали поспешных решений по типу: «Это должен быть интерфейс» или «Это должен быть базовый класс», или даже «Я не должен передавать конкретный класс в этот метод». Гибкость, чистота, поддерживаемость кода – все зависит только от вас. У всего есть свои преимущества и недостатки, а разработка программного обеспечения — это поиск правильного сочетания многих вещей.

3+