Что значит «программировать на интерфейс»?

814

Я видел это упомянутое несколько раз, и мне не ясно, что это значит. Когда и зачем ты это делаешь?

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

Это просто так, если вы должны были сделать:

IInterface classRef = new ObjectWhatever()

Вы могли бы использовать любой класс, который реализует IInterface? Когда вам нужно это сделать? Единственное, о чем я могу думать, это если у вас есть метод, и вы не уверены, какой объект будет передан, за исключением его реализации IInterface. Я не могу думать, как часто вам нужно будет это делать.

Кроме того, как вы могли бы написать метод, который принимает объект, который реализует интерфейс? Это возможно?

Damien
источник
3
Если вы помните, и ваша программа должна быть оптимальной, перед компиляцией вы можете поменять объявление интерфейса для фактической реализации. Как использование интерфейса добавляет уровень косвенности, который дает удар по производительности. Распределите свой код, запрограммированный на интерфейсы, хотя ...
Анде Тернер
18
@Анде Тернер: это плохой совет. 1). «Ваша программа должна быть оптимальной» не является хорошей причиной для замены интерфейсов! Затем вы говорите: «Распределите свой код, запрограммированный на интерфейсы, хотя ...», поэтому вы советуете, что с учетом данного требования (1) вы затем выпускаете неоптимальный код?!?
Митч Пшеница
74
Большинство ответов здесь не совсем правильные. Это вовсе не означает и даже не подразумевает «использовать ключевое слово интерфейса». Интерфейс - это спецификация того, как что-то использовать - синоним контракта (ищите его). Отдельно от этого является реализация, то есть, как выполняется этот контракт. Запрограммируйте только гарантии метода / типа, чтобы при изменении метода / типа таким образом, который все еще подчиняется контракту, он не нарушает код, использующий его.
jyoungdev
2
@ apollodude217, на самом деле это лучший ответ на всей странице. По крайней мере, для вопроса в названии, поскольку здесь есть по крайней мере 3 совершенно разных вопроса ...
Эндрю Спенсер
4
Основная проблема с такими вопросами заключается в том, что предполагается, что «программирование интерфейса» означает «оборачивать все в абстрактный интерфейс», что глупо, если учесть, что термин предшествует концепции абстрактных интерфейсов в стиле Java.
Джонатан Аллен

Ответы:

1635

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

Когда я впервые начал знакомиться с интерфейсами, я тоже был озадачен их актуальностью. Я не понимаю, зачем они тебе нужны. Если мы используем такой язык, как Java или C #, у нас уже есть наследование, и я рассматривал интерфейсы как более слабую форму наследования и думал: «Зачем беспокоиться?» В некотором смысле я был прав, вы можете рассматривать интерфейсы как своего рода слабую форму наследования, но помимо этого я наконец-то понял их использование в качестве языковой конструкции, рассматривая их как средство классификации общих черт или поведений, которые были продемонстрированы потенциально много не связанных классов объектов.

Например - скажем, у вас есть игра на SIM-карте и есть следующие классы:

class HouseFly inherits Insect {
    void FlyAroundYourHead(){}
    void LandOnThings(){}
}

class Telemarketer inherits Person {
    void CallDuringDinner(){}
    void ContinueTalkingWhenYouSayNo(){}
}

Очевидно, что эти два объекта не имеют ничего общего с точки зрения прямого наследования. Но вы могли бы сказать, что они оба раздражают.

Допустим, в нашей игре должна быть какая-то случайная вещь, которая раздражает игрока, когда они обедают. Это может быть a HouseFlyили a Telemarketerили оба - но как вы допускаете оба с одной функцией? И как вы просите каждый другой тип объекта «делать их надоедливые вещи» таким же образом?

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

interface IPest {
    void BeAnnoying();
}

class HouseFly inherits Insect implements IPest {
    void FlyAroundYourHead(){}
    void LandOnThings(){}

    void BeAnnoying() {
        FlyAroundYourHead();
        LandOnThings();
    }
}

class Telemarketer inherits Person implements IPest {
    void CallDuringDinner(){}
    void ContinueTalkingWhenYouSayNo(){}

    void BeAnnoying() {
        CallDuringDinner();
        ContinueTalkingWhenYouSayNo();
    }
}

Теперь у нас есть два класса, каждый из которых может раздражать по-своему. И они не должны происходить из одного и того же базового класса и иметь общие присущие им характеристики - они просто должны удовлетворять договору IPest- этот договор прост. Вы просто должны BeAnnoying. В связи с этим мы можем смоделировать следующее:

class DiningRoom {

    DiningRoom(Person[] diningPeople, IPest[] pests) { ... }

    void ServeDinner() {
        when diningPeople are eating,

        foreach pest in pests
        pest.BeAnnoying();
    }
}

Здесь у нас есть столовая, которая принимает количество посетителей и количество вредителей - обратите внимание на использование интерфейса. Это означает, что в нашем маленьком мире членом pestsмассива может быть Telemarketerобъект или HouseFlyобъект.

ServeDinnerМетод вызывается , когда ужин и наши люди в столовой должны есть. В нашей маленькой игре именно тогда наши вредители выполняют свою работу - каждый вредитель должен быть раздражен IPestинтерфейсом. Таким образом, мы можем легко иметь и то, Telemarketersи другое и HouseFlysбыть раздражающими по-своему - нас волнует только то, что у нас есть что-то в DiningRoomобъекте, который является вредным организмом, нам на самом деле все равно, что это такое, и они не могут иметь ничего общего с другим.

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

Питер Мейер
источник
3
Еще одна вещь, которую следует учитывать, - это то, что в некоторых случаях может быть полезно иметь интерфейс для вещей, которые «могут» раздражать, и иметь разнообразные объекты, реализуемые BeAnnoyingкак no-op; этот интерфейс может существовать вместо или в дополнение к интерфейсу для вещей, которые раздражают (если оба интерфейса существуют, интерфейс «вещи, которые раздражают» должен наследоваться от интерфейса «вещи, которые могут раздражать»). Недостаток использования таких интерфейсов состоит в том, что реализации могут быть обременены реализацией «раздражающего» числа методов-заглушек. Преимущество в том, что ...
суперкат
4
Методы не предназначены для представления абстрактных методов - их реализация не имеет отношения к вопросу, касающемуся интерфейсов.
Питер Мейер
33
Инкапсуляция поведения, такого как IPest, известна как шаблон стратегии на тот случай, если кому-то интересно
узнать
9
Интересно, что вы не указываете, что, поскольку объекты в IPest[]ссылках IPest, вы можете вызывать, BeAnnoying()потому что у них есть этот метод, в то время как вы не можете вызывать другие методы без приведения. Однако для каждого объекта BeAnnoying()будет вызван отдельный метод.
Д. Бен Нобл
4
Очень хорошее объяснение ... Мне просто нужно сказать это здесь: я никогда не слышал об интерфейсах, являющихся каким-то механизмом свободного наследования, но вместо этого я знаю, что наследование используется в качестве плохого механизма для определения интерфейсов (например, в обычном Python вы делай это все время).
Карлос Х Романо
283

Конкретный пример, который я привел студентам, состоит в том, что они должны написать

List myList = new ArrayList(); // programming to the List interface

вместо

ArrayList myList = new ArrayList(); // this is bad

Они выглядят точно так же в короткой программе, но если вы продолжите использовать myListв своей программе 100 раз, вы можете начать видеть разницу. Первое объявление гарантирует, что вы вызываете только те методы myList, которые определены Listинтерфейсом (поэтому никаких ArrayListконкретных методов). Если вы запрограммировали интерфейс таким образом, позже вы можете решить, что вам действительно нужно

List myList = new TreeList();

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

Преимущества становятся еще более очевидными (я думаю), когда вы говорите о параметрах метода и возвращаемых значениях. Возьмите это к примеру:

public ArrayList doSomething(HashMap map);

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

public List doSomething(Map map);

Теперь не имеет значения, какой Listвы возвращаете или какой тип Mapпередается в качестве параметра. Изменения, которые вы делаете внутри doSomethingметода, не заставят вас изменить вызывающий код.

Билл Ящерица
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Иветт
1
Очень четкое объяснение. Очень полезно
Самуэль Люсвата
У меня есть вопрос по причине, которую вы упомянули: «Первое объявление гарантирует, что вы вызываете только те методы myList, которые определены интерфейсом List (поэтому никаких специфичных для ArrayList методов). Если вы запрограммировали интерфейс таким образом, позже вы можете решить, что вам действительно нужен List myList = new TreeList (); вам нужно всего лишь изменить код в этом месте. " Может быть, я неправильно понял, мне интересно, почему вам нужно изменить ArrayList на TreeList, если вы хотите «гарантировать, что вы вызываете только методы из myList»?
user3014901
1
@ user3014901 Существует множество причин, по которым вы можете изменить тип списка, который вы используете. Например, можно улучшить производительность поиска. Дело в том, что если вы программируете на интерфейс List, это облегчает изменение вашего кода на другую реализацию позже.
Билл Ящерица
73

Программирование интерфейса говорит: «Мне нужна эта функциональность, и мне все равно, откуда она».

Рассмотрим (в Java) Listинтерфейс ArrayListи LinkedListконкретные классы. Если все, что меня волнует, это то, что у меня есть структура данных, содержащая несколько элементов данных, к которым я должен обращаться через итерацию, я бы выбрал List(и это 99% времени). Если я знаю, что мне нужно постоянное время вставки / удаления из любого конца списка, я мог бы выбрать LinkedListконкретную реализацию (или, более вероятно, использовать интерфейс очереди ). Если я знаю, что мне нужен произвольный доступ по индексу, я бы выбрал ArrayListконкретный класс.

kdgregory
источник
1
полностью согласен, т.е. независимость между тем, что сделано, и тем, как это делается. Разбивая систему на независимые компоненты, вы получаете систему, которая проста и
пригодна для
38

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

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

РЕДАКТИРОВАТЬ: Ниже приведена ссылка на статью, где Эрих Гамма обсуждает свою цитату «Программа для интерфейса, а не реализации».

http://www.artima.com/lejava/articles/designprinciples.html

tvanfosson
источник
4
Пожалуйста, прочитайте еще раз это интервью: Гамма, конечно, говорил о концепции интерфейса ОО, а не о JAVA или особом классе C # (ISomething). Проблема в том, что большинство людей, хотя он говорил о ключевом слове, теперь у нас много ненужных интерфейсов (ISomething).
Сильвен Родриг
Очень хорошее интервью. Пожалуйста, будьте осторожны с будущими читателями, в интервью четыре страницы. Я бы почти закрыл браузер, прежде чем увидеть его.
Объявление Infinitum
38

Программирование на интерфейсе не имеет абсолютно никакого отношения к абстрактным интерфейсам, как мы видим в Java или .NET. Это даже не концепция ООП.

Это означает, что вы не должны возиться с внутренностями объекта или структуры данных. Используйте абстрактный программный интерфейс или API для взаимодействия с вашими данными. В Java или C # это означает использование открытых свойств и методов вместо необработанного доступа к полям. Для C это означает использование функций вместо необработанных указателей.

РЕДАКТИРОВАТЬ: И с базами данных это означает использование представлений и хранимых процедур вместо прямого доступа к таблице.

Джонатан Аллен
источник
5
Лучший ответ. Гамма дает аналогичное объяснение здесь: artima.com/lejava/articles/designprinciples.html (см. Стр. 2). Он ссылается на концепцию ОО, но вы правы: это больше, чем это.
Сильвен Родриг
36

Вы должны посмотреть на инверсию контроля:

В таком случае вы бы не написали так:

IInterface classRef = new ObjectWhatever();

Вы бы написали что-то вроде этого:

IInterface classRef = container.Resolve<IInterface>();

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

Если мы оставим IoC вне таблицы, вы можете написать код, который знает, что он может общаться с объектом, который делает что-то конкретное , но не с тем, какой тип объекта или как он это делает.

Это пригодится при передаче параметров.

Что касается вашего вопроса в скобках «Кроме того, как вы могли бы написать метод, который принимает объект, который реализует интерфейс? Это возможно?», В C # вы просто использовали бы тип интерфейса для типа параметра, например так:

public void DoSomethingToAnObject(IInterface whatever) { ... }

Это включается прямо в «разговор с объектом, который делает что-то конкретное». Описанный выше метод знает, чего ожидать от объекта, что он реализует все в IInterface, но ему все равно, какой это тип объекта, только то, что он придерживается контракта, а именно интерфейс.

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

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

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

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

Теперь, поскольку элементы управления наследуются от предопределенных классов, которые мы не можем контролировать, мы можем сделать одну из трех вещей:

  1. Создайте поддержку для нашей системы перевода, чтобы точно определить, с каким типом управления она работает, и перевести правильные биты (кошмар обслуживания)
  2. Встроить поддержку в базовые классы (невозможно, поскольку все элементы управления наследуются от разных предопределенных классов)
  3. Добавить поддержку интерфейса

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

Лассе В. Карлсен
источник
31
Зачем упоминать IoC в самом начале, так как это только добавит путаницы.
Кевин Ле - Khnle
1
Согласитесь, я бы сказал, что программирование на основе интерфейсов - это всего лишь методика, позволяющая сделать IoC более простым и надежным.
terjetyl
28

Код для интерфейса Не реализация не имеет ничего общего с Java, как и с интерфейсом конструкции.

Эта концепция была широко известна в книгах «Паттерны / Банды четырех», но, скорее всего, была задолго до этого. Эта концепция, безусловно, существовала задолго до появления Java.

Конструкция Java Interface была создана, чтобы помочь в этой идее (среди прочего), и люди стали слишком сосредоточены на конструкции как центре значения, а не первоначальном намерении. Однако по этой причине у нас есть открытые и закрытые методы и атрибуты в Java, C ++, C # и т. Д.

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

И вы можете программировать для интерфейса, а не для реализации, даже не используя конструкцию интерфейса. Вы можете программировать на интерфейс, а не на реализацию в C ++, которая не имеет конструкции Interface. Вы можете интегрировать две массивные корпоративные системы гораздо надежнее, если они взаимодействуют через открытые интерфейсы (контракты), а не вызывают методы для объектов, внутренних по отношению к системам. Ожидается, что интерфейсы всегда будут реагировать одинаково ожидаемым образом при одинаковых входных параметрах; если реализовано для интерфейса, а не для реализации. Концепция работает во многих местах.

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

Билл Росмус
источник
1
Первое предложение говорит само за себя. Это должен быть принятый ответ.
Мадумлао
14

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

// if I want to add search capabilities to my application and support multiple search
// engines such as Google, Yahoo, Live, etc.

interface ISearchProvider
{
    string Search(string keywords);
}

тогда я мог бы создать GoogleSearchProvider, YahooSearchProvider, LiveSearchProvider и т. д.

// if I want to support multiple downloads using different protocols
// HTTP, HTTPS, FTP, FTPS, etc.
interface IUrlDownload
{
    void Download(string url)
}

// how about an image loader for different kinds of images JPG, GIF, PNG, etc.
interface IImageLoader
{
    Bitmap LoadImage(string filename)
}

затем создайте JpegImageLoader, GifImageLoader, PngImageLoader и т. д.

Большинство надстроек и плагинов работают от интерфейсов.

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

interface IZipCodeRepository
{
    IList<ZipCode> GetZipCodes(string state);
}

затем я мог бы создать XMLZipCodeRepository, SQLZipCodeRepository, CSVZipCodeRepository и т. д. Для своих веб-приложений я часто создаю XML-репозитории на ранней стадии, чтобы я мог что-то запустить и запустить до того, как база данных SQL будет готова. Когда база данных готова, я пишу SQLRepository, чтобы заменить версию XML. Остальная часть моего кода остается неизменной, поскольку он работает исключительно на интерфейсах.

Методы могут принимать интерфейсы, такие как:

PrintZipCodes(IZipCodeRepository zipCodeRepository, string state)
{
    foreach (ZipCode zipCode in zipCodeRepository.GetZipCodes(state))
    {
        Console.WriteLine(zipCode.ToString());
    }
}
Тодд смит
источник
10

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

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

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

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

Эд С.
источник
10

Там много объяснений, но сделать это еще проще. Возьмите для примера List. Можно реализовать список с помощью:

  1. Внутренний массив
  2. Связанный список
  3. Другие реализации

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

Вы можете использовать любой тип внутренней реализации, скажем, arrayреализацию. Но предположим, что вы хотите изменить реализацию по какой-то причине, например, из-за ошибки или производительности. Тогда вам просто нужно изменить объявление List<String> ls = new ArrayList<String>()на List<String> ls = new LinkedList<String>().

Нигде в коде вам не придется ничего менять; Потому что все остальное было построено на определении List.

Jatin
источник
8

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

Кевин Ле - Хнле
источник
Он полезен не только в том случае, если доступен более качественный драйвер, но и позволяет полностью сменить поставщиков баз данных.
Кен Лю
3
JDBC настолько плох, что нуждается в замене. Найдите другой пример.
Джошуа
JDBC плох, но не по какой-либо причине связан с интерфейсом или реализацией или уровнями абстракции. И поэтому, чтобы проиллюстрировать рассматриваемое понятие, оно просто идеально.
Эрвин Смут
8

Программирование на интерфейсах - это круто, это способствует слабой связи. Как упомянул @lassevk, Inversion of Control отлично подходит для этого.

Кроме того, посмотрите на твердые принципы . вот видео серия

Он проходит через жестко закодированный (сильно связанный пример), затем просматривает интерфейсы и, наконец, переходит к инструменту IoC / DI (NInject).

dbones
источник
7

Я опоздал на этот вопрос, но хочу упомянуть, что строка «Программа для интерфейса, а не для реализации» имела хорошее обсуждение в книге «Шаблоны проектирования GoF (Gang of Four)».

Указано на с. 18:

Программа для интерфейса, а не реализация

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

и, кроме того, это началось с:

Есть два преимущества манипулирования объектами исключительно с точки зрения интерфейса, определенного абстрактными классами:

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

Другими словами, не пишите это своим классам, чтобы у них был quack()метод для уток, а затем bark()метод для собак, потому что они слишком специфичны для конкретной реализации класса (или подкласса). Вместо этого напишите метод, используя имена, которые являются достаточно общими для использования в базовом классе, такие как giveSound()или move(), чтобы их можно было использовать для уток, собак или даже автомобилей, и тогда клиент ваших классов может просто сказать, .giveSound()а не думая о том, использовать quack()ли bark()или даже определить тип, прежде чем выдавать правильное сообщение для отправки объекту.

nonopolarity
источник
6

В дополнение к уже выбранному ответу (и различным информативным сообщениям здесь), я настоятельно рекомендую взять копию Head First Design Patterns . Это очень легко прочитать, и он ответит на ваш вопрос напрямую, объяснит, почему это важно, и покажет вам много шаблонов программирования, которые вы можете использовать, чтобы использовать этот принцип (и другие).

Уэйли
источник
5

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

e11s
источник
4

Это также хорошо для модульного тестирования, вы можете внедрить свои собственные классы (которые отвечают требованиям интерфейса) в класс, который зависит от него

Ричард
источник
4

Может быть полезно программировать на интерфейсы, даже если мы не зависим от абстракций.

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

  1. не позволяет нам делать неуместные в контексте вещи, и
  2. позволяет нам безопасно изменить реализацию в будущем.

Например, рассмотрим Personкласс , который реализует Friendи Employeeинтерфейс.

class Person implements AbstractEmployee, AbstractFriend {
}

В контексте дня рождения человека мы программируем Friendинтерфейс, чтобы предотвратить обращение с человеком как с Employee.

function party() {
    const friend: Friend = new Person("Kathryn");
    friend.HaveFun();
}

В контексте работы человека мы программируем Employeeинтерфейс, чтобы предотвратить размывание границ рабочего места.

function workplace() {
    const employee: Employee = new Person("Kathryn");
    employee.DoWork();
}

Отлично. Мы вели себя соответствующим образом в разных контекстах, и наше программное обеспечение работает хорошо.

В далеком будущем, если наш бизнес переходит на работу с собаками, мы можем довольно легко изменить программное обеспечение. Сначала мы создаем Dogкласс, который реализует оба Friendи Employee. Тогда мы благополучно изменим new Person()на new Dog(). Даже если обе функции имеют тысячи строк кода, это простое редактирование будет работать, потому что мы знаем, что верно следующее:

  1. Функция partyиспользует только FriendподмножествоPerson .
  2. Функция workplaceиспользует только EmployeeподмножествоPerson .
  3. Класс Dogреализует как Friendи Employeeинтерфейсы.

С другой стороны, если бы один из них partyили workplaceбыл запрограммирован против Person, был бы риск того, что у обоих будет Personспецифичный код. Переход от Personк Dogпотребовал бы, чтобы мы прочесали код, чтобы уничтожить любой Personспецифический код, Dogкоторый не поддерживает.

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

Шон Луттин
источник
1
Если у вас нет слишком широких интерфейсов, то есть.
Кейси
4

Если я пишу новый класс Swimmerдля добавления функциональности swim()и нужно использовать объект класса скажем Dog, и этот Dogкласс реализует интерфейс, Animalкоторый объявляет swim().

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

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

Абхишек Шривастава
источник
3

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

Например, я мог бы иметь интерфейс движения. Метод, который заставляет что-то «двигаться», и любой объект (Person, Car, Cat), который реализует интерфейс движения, может быть передан и задан для перемещения. Без метода каждый знает тип класса.

Damien
источник
3

Представьте, что у вас есть продукт под названием «Зебра», который можно расширить с помощью плагинов. Он находит плагины путем поиска DLL в некотором каталоге. Он загружает все эти библиотеки DLL и использует отражение, чтобы найти любые классы, которые реализуютIZebraPlugin , а затем вызывает методы этого интерфейса для связи с плагинами.

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

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

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

Дэниел Уорвикер
источник
3

C ++ объяснение.

Думайте об интерфейсе как о ваших открытых методах классов.

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

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

Смысл всего этого в том, что вы можете предоставить любой другой алгоритм / класс поиска, если он предоставляет интерфейс, от которого зависит Vector (поиск по пузырькам, последовательный поиск, быстрый поиск).

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

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

Что касается «упущения» в отношении того, как все работает; большой (по крайней мере, в C ++), поскольку именно так работает большая часть стандартной библиотеки TEMPLATE.

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

Это огромная тема и один из основополагающих принципов проектирования шаблонов.

Тре Барлоу
источник
3

В Java все эти конкретные классы реализуют интерфейс CharSequence:

CharBuffer, String, StringBuffer, StringBuilder

Эти конкретные классы не имеют общего родительского класса, кроме Object, поэтому ничто не связывает их, кроме того факта, что каждый из них имеет какое-то отношение к массивам символов, представляя их или манипулируя ими. Например, символы String нельзя изменить после создания экземпляра объекта String, тогда как символы StringBuffer или StringBuilder можно редактировать.

Тем не менее, каждый из этих классов способен соответствующим образом реализовать методы интерфейса CharSequence:

char charAt(int index)
int length()
CharSequence subSequence(int start, int end)
String toString()

В некоторых случаях классы библиотеки классов Java, которые раньше принимали String, были пересмотрены, чтобы теперь принимать интерфейс CharSequence. Поэтому, если у вас есть экземпляр StringBuilder, вместо извлечения объекта String (что означает создание экземпляра нового объекта), он может вместо этого просто передать сам StringBuilder, поскольку он реализует интерфейс CharSequence.

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

BufferedWriter, CharArrayWriter, CharBuffer, FileWriter, FilterWriter, LogStream, OutputStreamWriter, PipedWriter, PrintStream, PrintWriter, StringBuffer, StringBuilder, StringWriter, Writer

RogerV
источник
Это слишком плохие интерфейсы, как CharSequenceанемичные. Я хотел бы, чтобы Java и .NET позволили интерфейсам иметь реализацию по умолчанию, чтобы люди не урезали интерфейсы исключительно для минимизации стандартного кода. При любой допустимой CharSequenceреализации можно эмулировать большинство функций, Stringиспользуя только четыре вышеуказанных метода, но многие реализации могут выполнять эти функции гораздо более эффективно другими способами. К сожалению, даже если конкретная реализация CharSequenceхранит все в одном char[]и может выполнять много ...
суперкат
... такие операции, как indexOfбыстро, нет способа, чтобы вызывающий, не знакомый с конкретной реализацией, CharSequenceмог попросить его сделать это, вместо того, чтобы использовать его charAtдля изучения каждого отдельного символа.
суперкат
3

Короткая история: Почтальона просят пойти домой после дома и получить обложки (письма, документы, чеки, подарочные карты, заявление, любовное письмо) с адресом, написанным на нем для доставки.

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

Так что лучше оберните его крышкой (в нашей истории это интерфейс), тогда он отлично справится со своей работой.

Теперь работа почтальона - получать и доставлять только обложки (он не стал бы беспокоиться о том, что находится внутри обложки).

Создать тип interface не фактического типа, но реализуйте его с реальным типом.

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

Я приведу вам пример.

у вас есть интерфейс AirPlane, как показано ниже.

interface Airplane{
    parkPlane();
    servicePlane();
}

Предположим, у вас есть методы в вашем классе контроллеров плоскостей, такие как

parkPlane(Airplane plane)

а также

servicePlane(Airplane plane)

реализовано в вашей программе. Он не будет сломает ваш код. Я имею в виду, что это не должно измениться, если он принимает аргументы какAirPlane .

Потому что он будет принимать любой самолет, несмотря на фактический тип, flyer , highflyr,fighter и т.д.

Также в коллекции:

List<Airplane> plane; // Приниму все ваши самолеты.

Следующий пример прояснит ваше понимание.


У вас есть истребитель, который реализует его, так что

public class Fighter implements Airplane {

    public void  parkPlane(){
        // Specific implementations for fighter plane to park
    }
    public void  servicePlane(){
        // Specific implementatoins for fighter plane to service.
    }
}

То же самое для HighFlyer и других предметов:

public class HighFlyer implements Airplane {

    public void  parkPlane(){
        // Specific implementations for HighFlyer plane to park
    }

    public void  servicePlane(){
        // specific implementatoins for HighFlyer plane to service.
    }
}

Теперь думайте, что ваши классы контроллера используют AirPlaneнесколько раз,

Предположим, что ваш класс Controller - это ControlPlane, как показано ниже,

public Class ControlPlane{ 
 AirPlane plane;
 // so much method with AirPlane reference are used here...
}

Сюда приходит магия, поскольку вы можете создавать новые AirPlaneэкземпляры типов столько раз, сколько хотите, и вы не меняете код ControlPlaneкласса.

Вы можете добавить экземпляр ...

JumboJetPlane // implementing AirPlane interface.
AirBus        // implementing AirPlane interface.

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

Санджай Рабари
источник
2

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

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

Шивам Суганди
источник
2

Q: - ... "Не могли бы вы использовать какой-либо класс, который реализует интерфейс?"
A: - Да.

Q: - ... "Когда тебе нужно это сделать?"
A: - Каждый раз, когда вам нужен класс (ы), который реализует интерфейс (ы).

Примечание. Мы не смогли создать экземпляр интерфейса, не реализованного классом - True.

  • Почему?
  • Поскольку интерфейс имеет только прототипы методов, а не определения (только имена функций, а не их логика)

AnIntf anInst = new Aclass();
// мы можем сделать это только если Aclass реализует AnIntf.
// anInst будет иметь ссылку на класс.


Примечание: теперь мы можем понять, что произошло, если Bclass и Cclass реализовали один и тот же Dintf.

Dintf bInst = new Bclass();  
// now we could call all Dintf functions implemented (defined) in Bclass.

Dintf cInst = new Cclass();  
// now we could call all Dintf functions implemented (defined) in Cclass.

Что мы имеем: Те же прототипы интерфейса (имена функций в интерфейсе) и вызов различных реализаций.

Библиография: Прототипы - Википедия

Мерадж аль Максуд
источник
1

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

IInterface classRef = new ObjectWhatever()

Вы могли бы использовать любой класс, который реализует IInterface? Когда вам нужно это сделать?

Посмотрите на этот вопрос SE для хорошего примера.

Почему интерфейс для Java-класса должен быть предпочтительным?

делает использование интерфейса хит производительности?

если так сколько?

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

Как вы можете избежать этого без необходимости поддерживать два бита кода?

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

Один хороший пример использования: реализация шаблона стратегии:

Пример шаблона стратегии в реальном мире

Равиндра Бабу
источник
1

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

я считаю, что эти правила помогают мне:

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

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

обратите внимание, что есть два способа определить, когда их использовать. надеюсь это поможет.

j2emanue
источник
0

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

Модульное тестирование

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

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

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

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

public interface IPricable
{
    int Price { get; }
}

public interface ICar : IPricable

public abstract class Article
{
    public int Price { get { return ... } }
}

public class Car : Article, ICar
{
    // Price does not need to be defined here
}

Таким образом, копирование кода не является обязательным, но в то же время имеет преимущество использования автомобиля в качестве интерфейса (ICar).

Мишель Кейзерс
источник
0

Давайте начнем с некоторых определений:

Интерфейс n. Набор всех сигнатур, определенных операциями объекта, называется интерфейсом к объекту.

Введите n. Определенный интерфейс

Простой пример интерфейса , как определено выше , будет все методы объекта PDO , такие как query(), commit(),close() т. Д., В целом, а не отдельно. Эти методы, то есть его интерфейс, определяют полный набор сообщений, запросов, которые могут быть отправлены объекту.

Типа , как определено выше , представляет собой конкретный интерфейс. Я буду использовать выдуманный интерфейс формы для демонстрации: draw(), getArea(), и getPerimeter()т.д ..

Если объект имеет тип базы данных, мы имеем в виду, что он принимает сообщения / запросы интерфейса базы данных query()и commit()т. Д. Объекты могут быть разных типов. Вы можете иметь объект базы данных типа фигуры, если он реализует свой интерфейс, и в этом случае это будет подтип .

Многие объекты могут иметь разные интерфейсы / типы и реализовывать этот интерфейс по-разному. Это позволяет нам заменять объекты, позволяя нам выбирать, какой из них использовать. Также известен как полиморфизм.

Клиент будет знать только интерфейс, а не реализацию.

Таким образом , в сущности программирования на интерфейс предполагает сделать некоторый тип абстрактного класса , такой как Shapeс интерфейсом только указанный то есть draw(), getCoordinates(), и getArea()т.д .. И тогда есть различные классы конкретные реализации этих интерфейсов , таких как класс Circle, Square класса Triangle класса. Следовательно, программа для интерфейса не является реализацией.

Роберт Роча
источник
0

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

ndoty
источник