Что значит «программировать на интерфейс»? - PullRequest
768 голосов
/ 21 декабря 2008

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

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

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

IInterface classRef = new ObjectWhatever()

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

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

Ответы [ 32 ]

1534 голосов
/ 21 декабря 2008

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

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

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

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

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

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

Скажем, в нашей игре должна быть какая-то случайная вещь , которая раздражает игрока, когда они обедают. Это может быть HouseFly или 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, который является вредным организмом, нам все равно что это такое, и они не могли иметь ничего общего с другими.

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

256 голосов
/ 21 декабря 2008

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

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, не заставят вас изменить код вызова.

69 голосов
/ 21 декабря 2008

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

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

36 голосов
/ 21 декабря 2008

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

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

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

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

35 голосов
/ 21 декабря 2008

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

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

IInterface classRef = new ObjectWhatever();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

31 голосов
/ 19 июля 2012

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

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

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

24 голосов
/ 14 апреля 2016

Код для интерфейса Не реализация не имеет НИЧЕГО общего с Java и конструкцией интерфейса.

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

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

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

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

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

12 голосов
/ 21 декабря 2008

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

// 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 голосов
/ 21 декабря 2008

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

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

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

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

8 голосов
/ 21 декабря 2008

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

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

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

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