Понимание «программирования для интерфейса»

30

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

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

Типичным примером в Java является использование:

List myList = new ArrayList();вместо ArrayList myList = new ArrayList();.

У меня есть два вопроса по этому поводу:

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

  2. Есть ли еще способы «программирования на интерфейс»? Или «объявление переменной как интерфейса, а не конкретной реализации» - единственная реализация этой концепции?

Я не говорю об интерфейсе конструкции Java . Я говорю о принципе ОО «программирование на интерфейс, а не на реализацию». В этом принципе мировой «интерфейс» относится к любому «супертипу» класса - интерфейсу, абстрактному классу или простому суперклассу, который является более абстрактным и менее конкретным, чем его более конкретные подклассы.

Авив Кон
источник
Возможный дубликат Почему полезны интерфейсы?
комнат
1
Этот ответ дает вам простой для понимания пример programmers.stackexchange.com/a/314084/61852
Tulains Córdova

Ответы:

46

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

Это не правильно . Или, по крайней мере, это не совсем правильно.

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

Основная идея заключается в том, что домены меняются гораздо медленнее, чем программное обеспечение. Скажем, у вас есть программное обеспечение для отслеживания вашего списка покупок. В 80-х это программное обеспечение работало с командной строкой и некоторыми плоскими файлами на дискете. Тогда вы получили пользовательский интерфейс. Тогда вы можете положить список в базе данных. Позже он может быть перемещен в облако или мобильные телефоны или интеграцию с Facebook.

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

Telastyn
источник
Спасибо за ответ. Судя по тому, что вы написали, я думаю, что понимаю, что означает «программирование для интерфейса» и каковы его преимущества. Но у меня есть один вопрос - Наиболее распространенный конкретный пример для этой концепции заключается в следующем: при создании ссылки на объект сделайте ссылочный тип интерфейсным типом, который реализует этот объект (или суперкласс, который этот объект наследует), вместо создания ссылочного типа тип объекта. (Ака, List myList = new ArrayList()вместо ArrayList myList = new ArrayList(). (Вопрос в следующем комментарии)
Авив Кон
Мой вопрос: не могли бы вы дать мне больше примеров для мест в реальном мире кода, где имеет место принцип «программирования на интерфейс»? Кроме общего примера, который я описал в последнем комментарии?
Авив Кон
6
NO . List/ ArrayListэто совсем не то, о чем я говорю. Это больше похоже на предоставление Employeeобъекта, а не набора связанных таблиц, которые вы используете для хранения записи сотрудника. Или предоставляя интерфейс для перебора песен, и не заботясь о том, перемешаны ли эти песни, или на компакт-диске, или в потоковом режиме из Интернета. Это просто последовательность песен.
Теластин
1
Существование «кнопки турбо»: en.wikipedia.org/wiki/Turbo_button - это реальный пример аналогии с дискетами в реальном мире.
JimmyJames
1
@AvivCohn Я бы предложил SpringFramework в качестве хорошего примера. Все в Spring может быть улучшено или настроено путем создания собственных интерфейсов и основного поведения Springs - функции будут работать должным образом ... Или в случае настроек, как вы ожидали. Программирование на интерфейсе также является лучшей стратегией для разработки интегрированных сред. Снова Spring делает с его Spring-интеграции. Во время разработки программирование интерфейса - это то, что мы делаем с UML. Или это то, что я бы сделал. Абстрагируйся от того, как это происходит, и сфокусируйся на том, что делать.
Laiv
19

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

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

Например, скажем, API предоставляет вам какое-то непрозрачное значение, которое является «дескриптором» чего-то внутреннего. Ваши знания могут сказать вам, что этот дескриптор действительно является указателем, и вы можете разыменовать его и получить доступ к некоторому значению, которое может позволить вам легко выполнить некоторую задачу, которую вы хотите выполнить. Но интерфейс не предоставляет вам такую ​​возможность; это ваше знание конкретной реализации, которая делает.

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

Большой пример этого - программы, написанные для Windows. WinAPI - это интерфейс, но многие люди использовали приемы, которые работали из-за конкретной реализации, скажем, в Windows 95. Эти приемы, возможно, делали их программы быстрее или позволяли им делать вещи в меньшем количестве кода, чем было бы необходимо в противном случае. Но эти хитрости также означали, что программа зависала в Windows 2000, потому что API там реализован по-другому. Если бы программа была достаточно важной, Microsoft могла бы на самом деле пойти дальше и добавить некоторый взлом к ​​их реализации, чтобы программа продолжала работать, но цена этого - повышенная сложность (со всеми вытекающими проблемами) кода Windows. Это также усложняет жизнь пользователям Wine, потому что они также пытаются реализовать WinAPI, но могут обратиться только к документации, чтобы узнать, как это сделать,

Себастьян Редл
источник
Это хороший момент, и я часто слышу это в определенных контекстах.
Теластин
Я понимаю вашу точку зрения. Итак, давайте посмотрим, смогу ли я адаптировать то, что вы говорите, к общему программированию: допустим, у меня есть класс (класс A), который использует функциональные возможности абстрактного класса B. Классы C и D наследуют класс B - они обеспечивают конкретную реализацию того, что это класс, как говорят, делать. Если класс A использует непосредственно класс C или D, это называется «программированием для реализации», что не является очень гибким решением. Но если класс A использует ссылку на класс B, который впоследствии может быть установлен на реализацию C или реализацию D, это делает вещи более гибкими и обслуживаемыми. Это верно?
Авив Кон
Если это правильно, тогда мой вопрос - есть ли более конкретные примеры для «программирования на интерфейсе», кроме «общего использования ссылки на интерфейс, а не ссылки на конкретный класс»?
Авив Кон
2
@AvivCohn Немного поздно в этом ответе, но один конкретный пример - всемирная паутина. Во время войн браузеров (эпоха IE 4) веб-сайты были написаны не в соответствии с тем, что говорится в какой-либо спецификации, а с причудами некоторых браузеров (Netscape или IE). Это было в основном программирование для реализации вместо интерфейса.
Себастьян Редл
9

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

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

Например, рассмотрим ILoggerинтерфейс, который в настоящее время реализован как конкретный LogToEmailLoggerкласс. LogToEmailLoggerКласс предоставляет все ILoggerметоды и свойства, но случается, есть свойство конкретной реализации sysAdminEmail.

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

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

В этом смысле кодирование интерфейса ослабляет связь .

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

Например, представьте себе , у меня есть игра со следующими интерфейсами I2DRenderable, I3DRenderable, IUpdateable. Нередко отдельные компоненты игры имеют как 2D, так и 3D визуализируемый контент. Другие компоненты могут быть только 2D и другие только 3D.

Если 2D-рендеринг выполняется одним модулем, то имеет смысл сохранить коллекцию I2DRenderables. Не имеет значения, являются ли объекты в его коллекции также, I3DRenderableили IUpdatebleкак другие модули будут отвечать за обработку этих аспектов объектов.

Хранение визуализируемых объектов в виде списка I2DRenderableснижает сложность класса рендеринга. Логика 3D-рендеринга и обновления не имеет значения, поэтому эти аспекты дочерних объектов можно и нужно игнорировать.

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

MetaFight
источник
4

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

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

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

Это также означает, что ваш код должен быть более стабильным. Придерживаясь интерфейса, вы не должны нарушать чужой код. Когда вам действительно нужно изменить интерфейс, вы можете выпустить новую основную версию (1.abc to 2.xyz) API, которая сигнализирует о существенных изменениях в интерфейсе новой версии.

Как отмечает @Doval в комментариях к этому ответу, существует также множество ошибок. Я думаю, что все это сводится к инкапсуляции. Так же, как вы использовали бы его для объектов в объектно-ориентированном дизайне, эта концепция также полезна на более высоком уровне.

Encaitar
источник
1
Преимущество, которое обычно упускают из виду, - это локальность ошибок. Скажем, вам нужна карта, и вы реализуете ее, используя двоичное дерево. Чтобы это работало, ключи должны иметь некоторый порядок, и вам нужно поддерживать инвариант, чтобы ключи, которые «меньше», чем ключ текущего узла, находились в левом поддереве, а ключи, которые «больше чем», были включены. правильное поддерево. Когда вы скрываете реализацию Map за интерфейсом, если поиск Map идет не так, как вы знаете, ошибка должна быть в модуле Map. Если он обнаружен, ошибка может быть где угодно в программе. Для меня это главное преимущество.
Доваль
4

Аналогия с реальным миром может помочь:

Сетевой штепсель в интерфейсе.
Да; эта трехконтактная вещь на конце шнура питания от вашего телевизора, радио, пылесоса, стиральной машины и т.д ..

Любое устройство, имеющее основной штепсель (т.е. реализующий интерфейс «имеет сетевой штепсель»), может обрабатываться точно так же; все они могут быть подключены к сетевой розетке и могут получать питание от этой розетки.

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

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

Фил В.
источник
1

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

В архитектуре программного обеспечения существует множество естественных границ. Общие примеры включают

  • Граница сети между клиентским и серверным процессами
  • Граница API между приложением и сторонней библиотекой
  • Внутренняя граница кода между различными бизнес-доменами в программе

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

Последствия этого:

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

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

Майкл Шоу
источник