Що означає "програмувати на інтерфейс"?


813

Я це бачив уже кілька разів, і мені не ясно, що це означає. Коли і навіщо ви це робили?

Я знаю, що роблять інтерфейси, але той факт, що мені це не зрозуміло, змушує мене думати, що я пропускаю їх правильне використання.

Це просто так, якби ви це робили:

IInterface classRef = new ObjectWhatever()

Ви можете використовувати будь-який клас, який реалізує IInterface? Коли вам це потрібно було зробити? Єдине, про що я можу подумати - це якщо у вас є метод і ви не впевнені, який об’єкт буде переданий, крім його реалізації IInterface. Я не можу подумати, як часто вам потрібно це робити.

Крім того, як ви могли написати метод, який бере об’єкт, що реалізує інтерфейс? Це можливо?


3
Якщо ви можете пам’ятати, і ваша програма повинна бути оптимальною, безпосередньо перед компіляцією, можливо, ви захочете поміняти декларацію інтерфейсу на фактичну реалізацію. Оскільки за допомогою інтерфейсу додається рівень непрямості, що дає продуктивність. Розподіліть свій код, запрограмований на інтерфейси, хоча ...
Ande Turner

18
@Ande Turner: це погана порада. 1). "Ваша програма повинна бути оптимальною" - це не вагома причина для заміни інтерфейсів! Тоді ви говорите "Поширюйте свій код, запрограмований на інтерфейси, хоча ...", тож ви радите, що за вказаної вимоги (1) ви випустите суб-оптимальний код?!?
Мітч Пшеничний

74
Більшість відповідей тут не зовсім правильні. Це зовсім не означає або навіть означає "використання ключового слова інтерфейсу" взагалі. Інтерфейс - це специфіка, як використовувати щось - синонім контракту (дивіться його). Окремим від цього є реалізація, яка полягає в тому, як цей договір виконується. Програмуйте лише проти гарантій методу / типу, щоб, коли метод / тип змінено таким чином, що все ще підкоряється договору, він не порушує код, використовуючи його.
jyoungdev

2
@ apollodude217 - це насправді найкраща відповідь на всій сторінці. Принаймні щодо питання в заголовку, оскільки тут є щонайменше 3 зовсім інші питання ...
Ендрю Спенсер

4
Фундаментальна проблема таких питань полягає в тому, що вона передбачає, що "програмування на інтерфейс" означає "загортання всього в абстрактний інтерфейс", що нерозумно, якщо врахувати, що термін передує концепції абстрактних інтерфейсів стилю Java.
Джонатан Аллен

Відповіді:


1633

Тут є кілька чудових відповідей на ці питання, які містять у собі всілякі детальні відомості про інтерфейси та слабкий код з’єднання, інверсію управління тощо. Є кілька досить бурхливих дискусій, тому я хотів би скористатися можливістю, щоб трохи розбити речі, щоб зрозуміти, чому інтерфейс корисний.

Коли я вперше почав стикатися з інтерфейсами, я теж був розгублений щодо їх актуальності. Я не розумів, навіщо вони вам потрібні. Якщо ми використовуємо таку мову, як 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що є шкідником, нас насправді не хвилює, що це таке, і в них нічого не може бути спільне з іншими.

Цей дуже надуманий приклад псевдо-коду (який тягнувся набагато довше, ніж я передбачав), просто призначений для ілюстрації тієї речі, яка нарешті включила для мене світло з точки зору того, коли ми можемо використовувати інтерфейс. Я заздалегідь прошу вибачення за примхливість прикладу, але сподіваюся, що це допоможе у вашому розумінні. І, безумовно, інші опубліковані тут відповіді насправді охоплюють гаму використання сьогодні інтерфейсів у моделях дизайну та методології розробки.


3
Інша річ, яку слід врахувати, полягає в тому, що в деяких випадках може бути корисним інтерфейс для речей, які "можуть" дратувати, і мати різноманітність об'єктів, що реалізуються BeAnnoyingяк неоперативні ; цей інтерфейс може існувати замість або на додаток до інтерфейсу для речей, які дратують (якщо обидва інтерфейси існують, інтерфейс "речі, які дратують" повинен успадковуватись від інтерфейсу "речі, які можуть дратувати"). Недоліком використання таких інтерфейсів є те, що реалізація може обтяжуватися реалізацією "дратівливої" кількості методів заглушки. Перевага в тому, що ...
supercat

4
Методи не призначені для відображення абстрактних методів - їх реалізація не має значення для питання, що зосереджено на інтерфейсах.
Пітер Мейєр

33
Інкапсуляція поведінки, наприклад, IPest, відома як стратегія, на випадок, якщо хтось зацікавлений у
наданні

9
Цікаво, що ви не вказуєте на те, що оскільки об'єкти в IPest[]посиланнях IPest є посиланням, ви можете викликати, BeAnnoying()оскільки у них є такий метод, тоді як ви не можете викликати інші методи без виступу. Однак кожен об'єкт BeAnnoying()буде називатися індивідуальним методом.
Д. Бен Нобл

4
Дуже добре пояснення ... Мені просто потрібно це сказати: я ніколи не чув, щоб інтерфейси були якимось вільним механізмом успадкування, але натомість я знаю, як спадкування використовується як поганий механізм визначення інтерфейсів (наприклад, у звичайному Python you робіть це постійно).
Карлос Х Романо

282

Конкретний приклад, який я давав студентам, - це те, що вони повинні писати

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

замість

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

Вони виглядають точно так само в короткій програмі, але якщо ви продовжите використовувати myList100 разів у своїй програмі, ви можете почати бачити різницю. Перша декларація гарантує, що ви викликаєте лише методи myList, визначені Listінтерфейсом (тому ніяких ArrayListконкретних методів немає ). Якщо ви запрограмували цей інтерфейс таким чином, згодом ви можете вирішити, що вам дійсно потрібно

List myList = new TreeList();

і вам потрібно лише змінити свій код у тому місці. Ви вже знаєте, що решта вашого коду не робить нічого, що буде порушено зміною реалізації, оскільки ви запрограмовані на інтерфейс .

Переваги ще очевидніші (я думаю), коли ви говорите про параметри методу та значення повернення. Візьмемо це для прикладу:

public ArrayList doSomething(HashMap map);

Цей декларація методу пов'язує вас з двома конкретними реалізаціями ( ArrayListі HashMap). Щойно цей метод викликається з іншого коду, будь-які зміни цих типів, ймовірно, означають, що вам доведеться також змінити код виклику. Краще було б запрограмувати інтерфейси.

public List doSomething(Map map);

Тепер не має значення, який саме Listви повертаєтесь, чи який тип Mapпередається як параметр. Зміни, внесені всередину doSomethingметоду, не змусять вас змінити код виклику.


Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Іветт

Дуже чітке пояснення. Дуже корисно
samuel luswata

У мене виникає питання про причину, про яку ви згадали "Перша декларація гарантує, що ви викликаєте лише методи myList, які визначені інтерфейсом List (тому немає специфічних методів ArrayList). Якщо ви програмували цей інтерфейс таким чином, пізніше про вас Ви можете вирішити, що вам справді потрібен Список myList = new TreeList (); і вам потрібно лише змінити свій код у тому місці. " Можливо, я неправильно зрозумів, мені цікаво, чому вам потрібно змінити ArrayList на TreeList, якщо ви хочете "гарантувати, що ви лише викликаєте методи в myList"?
користувач3014901

1
@ user3014901 Ви можете змінити тип списку, який ви використовуєте, з будь-якої кількості причин. Наприклад, можна мати кращу ефективність пошуку. Справа в тому, що якщо ви запрограмуєте на інтерфейс List, це полегшить пізніше змінити код на іншу реалізацію.
Білл Ящірка

73

Програмування інтерфейсу говорить: "Мені потрібна ця функціональність і мені все одно, звідки вона береться".

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


1
повністю згодні, тобто незалежність між тим, що робиться, і як це робиться. Розбивши систему на незалежні компоненти, ви закінчите систему, яка є простою та багаторазовою (див. Простий хлопець, який створив Clojure, див. Simple Made Easy )
beluchin

38

Використання інтерфейсів є ключовим фактором для полегшення перевірки коду на додаток до видалення непотрібних з’єднань між класами. Створюючи інтерфейс, який визначає операції над вашим класом, ви дозволяєте класам, які хочуть використовувати цю функціональність, можливість використовувати його, не залежно від вашого класу реалізації безпосередньо. Якщо згодом ви вирішите змінити та використати іншу реалізацію, вам потрібно змінити лише ту частину коду, де реалізована інстанція. Решту коду не потрібно змінювати, оскільки це залежить від інтерфейсу, а не від класу реалізації.

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

EDIT: Нижче посилання на статтю, де Еріх Гамма обговорює свою цитату "Програма на інтерфейс, а не реалізація".

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


3
Будь ласка, прочитайте ще раз це інтерв'ю: Гамма, звичайно, говорив про концепцію інтерфейсу OO, а не про JAVA або спеціальний клас C # (ISomething). Проблема полягає в тому, що більшість людей, хоча він говорив про ключове слово, тому зараз у нас багато непотрібних інтерфейсів (ISomething).
Сільвен Родріг

Дуже хороше інтерв'ю. Будь ласка, будьте уважні до майбутніх читачів, в інтерв'ю є чотири сторінки. Я майже закрив браузер, перш ніж його побачити.
Ad Infinitum

37

Програмування інтерфейсу абсолютно не має нічого спільного з абстрактними інтерфейсами, як ми бачимо в Java або .NET. Це навіть не концепція ООП.

Це означає, що не варто возитися з внутрішніми об'єктами чи структурою даних. Використовуйте інтерфейс абстрактного програмного забезпечення або API для взаємодії з вашими даними. У Java або C # це означає використання загальнодоступних властивостей та методів замість необмеженого доступу до поля. Для C це означає використання функцій замість необроблених покажчиків.

EDIT: І з базами даних це означає використовувати представлення даних і збережені процедури замість прямого доступу до таблиці.


5
Найкраща відповідь. Гамма дає подібне пояснення тут: artima.com/lejava/articles/designprinciples.html (див. Сторінку 2). Він посилається на концепцію ОО, але ви маєте рацію: це більше, ніж це.
Сільвейн Родріг

36

Ви повинні вивчити інверсію управління:

У такому сценарії ви б не писали цього:

IInterface classRef = new ObjectWhatever();

Ви б написали щось подібне:

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

Це увійде в налаштування на containerоб'єкті, засноване на правилах , і побудує для вас фактичний об'єкт, який може бути ObjectWever. Важливим є те, що ви можете замінити це правило на те, що взагалі використовувало інший тип об'єкта, і ваш код все одно буде працювати.

Якщо ми залишимо IoC поза столом, ви можете написати код, який знає, що він може говорити з об’єктом, який робить щось конкретне , але не тим, який тип об'єкта чи як це робить.

Це стане в нагоді при передачі параметрів.

Що стосується вашого круглих питань "Крім того, як ви могли написати метод, який використовує об'єкт, що реалізує інтерфейс? Це можливо?", В C # ви просто використовуєте тип інтерфейсу для типу параметра, наприклад:

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

Це підключається прямо до "розмови з об'єктом, який робить щось конкретне". Метод, визначений вище, знає, чого очікувати від об'єкта, що він реалізує все в IInterface, але йому не важливо, який тип об'єкта це, лише те, що він дотримується контракту, що таке інтерфейс.

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

Це краса інтерфейсів. Ви можете написати фрагмент коду, який знає, що він передасть йому об'єкти, від яких можна очікувати певної поведінки. Це не байдуже, що це за предмет, лише те, що він підтримує необхідну поведінку.

Дозвольте навести конкретний приклад.

У нас є спеціально створена система перекладу для форм Windows. Ця система проходить цикл через елементи управління на формі та перекладає текст у кожну. Система знає, як керувати базовими елементами управління, як-от властивість type-of-control-that-has-a-Text та подібні базові речі, але для чого-небудь базового це не вистачає.

Тепер, оскільки елементи управління успадковують попередньо визначені класи, над якими ми не маємо контролю, ми могли б зробити одну з трьох речей:

  1. Створіть підтримку нашої системи перекладу, щоб виявити конкретно, з яким типом управління вона працює, і перекладати правильні біти (кошмар технічного обслуговування)
  2. Побудувати підтримку в базові класи (неможливо, оскільки всі елементи управління успадковуються від різних попередньо визначених класів)
  3. Додати підтримку інтерфейсу

Отже, ми зробили nr. 3. Усі наші елементи управління реалізують ILocalizable, що є інтерфейсом, який дає нам один метод, можливість перекладати "себе" в контейнер тексту / правил перекладу. Таким чином, формі не потрібно знати, який тип керування він знайшов, лише те, що він реалізує певний інтерфейс, і знає, що існує метод, за допомогою якого він може викликати локалізацію управління.


31
Навіщо згадувати про IoC на самому початку, оскільки це лише додасть більше сум'яття.
Кевін Ле - Кнле

1
Погодьтеся, я б сказав, що програмування на інтерфейсах - це лише техніка, щоб зробити IoC простішим і надійнішим.
terjetyl

28

Код до інтерфейсу Не реалізація не має нічого спільного з Java, ні з її інтерфейсною конструкцією.

Ця концепція стала відомою у книгах "Шаблони / Банда чотирьох", але, швидше за все, вона була задовго до цього. Концепція, безумовно, існувала задовго до існування Java

Конструкт Java-інтерфейсу був створений, щоб допомогти в цій ідеї (серед іншого), і люди стали занадто зосереджені на конструкції як на центрі сенсу, а не на первісному намірі. Однак це є причиною, що у нас є публічні та приватні методи та атрибути на Java, C ++, C # тощо.

Це означає просто взаємодіяти з публічним інтерфейсом об'єкта чи системи. Не хвилюйтесь і не передбачте, як він робить те, що робить всередині. Не хвилюйтеся, як це реалізовано. В об'єктно-орієнтованому коді, тому ми маємо публічні та приватні методи / атрибути. Ми покликані використовувати загальнодоступні методи, оскільки приватні методи існують лише для використання всередині класу. Вони складають реалізацію класу і можуть бути змінені за потребою, не змінюючи публічний інтерфейс. Припустимо, що щодо функціональності метод класу буде виконувати ту саму операцію з тим самим очікуваним результатом кожного разу, коли ви викликаєте його з тими ж параметрами. Це дозволяє авторові змінити спосіб роботи класу, його реалізацію, не порушуючи взаємодії з ним людей.

І ви можете запрограмувати на інтерфейс, а не на реалізацію без використання інтерфейсу. Ви можете запрограмувати на інтерфейс не реалізацію на C ++, яка не має конструкції інтерфейсу. Ви можете інтегрувати дві масивні корпоративні системи набагато міцніше, якщо вони взаємодіють через загальнодоступні інтерфейси (контракти), а не викликати методи на внутрішніх об'єктах. Очікується, що інтерфейси завжди реагуватимуть тим же самим очікуваним способом при одних і тих же вхідних параметрах; якщо впроваджено в інтерфейс, а не в реалізацію. Концепція працює у багатьох місцях.

Похитайте думкою, що інтерфейси Java мають щось спільне з концепцією "Програма на інтерфейс, а не реалізація". Вони можуть допомогти застосувати концепцію, але вони є не є поняттям.


1
У першому реченні все сказано. Це має бути прийнятою відповіддю.
madumlao

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 тощо.

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

Ще одне популярне використання - для шаблону сховища. Скажіть, я хочу завантажити список поштових індексів з різних джерел

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

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

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

Я вирішив, що найкраще створити абстрактний базовий клас для кожного типу керування, який би містив майже всю реальну логіку, а потім отримані типи, щоб подбати про відмінності між двома компонентами. Однак базові класи не змогли б виконувати операції над цими компонентами, якби я весь час турбувався про типи (ну, вони могли б бути, але у кожному методі було б твердження "якщо") .

Я визначив простий інтерфейс для цих компонентів, і всі базові класи спілкуються з цим інтерфейсом. Тепер, коли я щось міняю, він майже "просто працює" скрізь, і у мене немає дублювання коду.


9

Багато пояснень там, але зробити це ще простіше. Візьмемо для прикладу a List. Список можна реалізувати за допомогою:

  1. Внутрішній масив
  2. Зв'язаний список
  3. Інші реалізації

Побудувавши інтерфейс, скажімо, a List. Ви кодуєте лише визначення списку або те, що Listозначає насправді.

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

Ніде більше в коді, вам доведеться нічого іншого не змінювати; Тому що все інше будувалося на визначенні List.


8

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


Це не просто корисно у випадку, якщо стане доступний кращий драйвер, він дозволяє повністю змінити постачальників баз даних.
Кен Лю

3
JDBC настільки поганий, що потребує заміни. Знайдіть інший приклад.
Джошуа

JDBC поганий, але не з будь-якої причини пов'язаний з інтерфейсом проти реалізації або рівнями абстракції. І тому для того, щоб проілюструвати цю концепцію, вона просто ідеальна.
Ервін Смоут

8

Програмування на інтерфейси є приголомшливим, воно сприяє слабкому з'єднанню. Як згадував @lassevk, Інверсія управління - це велике використання цього.

Крім того, подивіться на принципи SOLID . ось відеосеріал

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


7

Я запізнююсь із цим питанням, але тут хочу зазначити, що рядок "Програма на інтерфейс, а не реалізація" мала добру дискусію в книзі "Шаблони дизайну" GoF (Gang of Four).

У ньому зазначено, на с. 18:

Програма на інтерфейс, а не на реалізацію

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

і вище цього, він починався з:

Є два переваги для маніпулювання об'єктами виключно з точки зору інтерфейсу, визначеного абстрактними класами:

  1. Клієнти не знають про конкретні типи об’єктів, якими вони користуються, доки об’єкти дотримуються інтерфейсу, який очікують клієнти.
  2. Клієнти не знають про класи, які реалізують ці об'єкти. Клієнти знають лише про абстрактні класи, що визначають інтерфейс.

Отже, іншими словами, не пишіть це своїм класам так, щоб він мав quack()метод для качок, а потім bark()метод для собак, оскільки вони занадто специфічні для конкретної реалізації класу (або підкласу). Замість цього напишіть метод, використовуючи імена, які є загальними для використання в базовому класі, наприклад, giveSound()або move(), щоб їх можна було використовувати для качок, собак чи навіть автомобілів, і тоді клієнт ваших занять може просто сказати, .giveSound()а не роздумуючи над тим, чи використовувати quack()або bark()навіть визначати тип перед тим, як видати правильне повідомлення, яке потрібно надіслати об’єкту.


6

На додаток до вже вибраної відповіді (та різних інформативних публікацій тут), я б дуже рекомендував захопити копію шаблонів дизайну Head First . Це дуже легко прочитати і відповість на ваше запитання безпосередньо, пояснить, чому це важливо, і покаже вам багато моделей програмування, якими ви можете скористатися для використання цього принципу (та інших).


5

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


4

Це також добре для Unit Testing, ви можете вводити власні класи (які відповідають вимогам інтерфейсу) до класу, який залежить від нього.


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

Отже, щоб отримати це право, перевага інтерфейсу полягає в тому, що я можу відокремити виклик методу від будь-якого конкретного класу. Замість того, щоб створити екземпляр інтерфейсу, де реалізація дається з того класу, який я вибрав, який реалізує цей інтерфейс. Таким чином, я дозволяю мені мати багато класів, які мають схожі, але дещо різні функціональні можливості, а в деяких випадках (випадки, пов’язані з наміром інтерфейсу) не цікаво, який це об'єкт.

Наприклад, у мене міг бути інтерфейс руху. Метод, який змушує щось "рухатись", і будь-який об'єкт ("Особа", "Автомобіль", "Кішка"), який реалізує інтерфейс руху, може бути переданий і наказаний рухатися. Без методу кожен не знає типу класу.


3

Уявіть, що у вас є продукт під назвою "Зебра", який можна розширити плагінами. Він знаходить плагіни шляхом пошуку DLL-файлів у деякому каталозі. Він завантажує всі ці DLL та використовує відображення для пошуку будь-яких класів, які реалізуютьсяIZebraPlugin , а потім викликає методи цього інтерфейсу для спілкування з плагінами.

Це робить його повністю незалежним від будь-якого конкретного класу плагінів - не байдуже, що це за класи. Дбає лише про те, що вони відповідають специфікації інтерфейсу.

Інтерфейси - це спосіб визначення точок розширення, як це. Код, який спілкується з інтерфейсом, більш вільно пов'язаний - адже він взагалі не пов'язаний з будь-яким іншим конкретним кодом. Він може взаємодіяти з плагінами, написаними роками пізніше людьми, які ніколи не зустрічалися з оригінальним розробником.

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


3

C ++ пояснення.

Розглядайте інтерфейс як загальнодоступні методи своїх класів.

Потім ви можете створити шаблон, який "залежить" від цих публічних методів, щоб виконувати свою власну функцію (він робить виклики функцій, визначені в класі загальнодоступним інтерфейсом). Скажімо, цей шаблон є контейнером, як векторний клас, і інтерфейс, від якого залежить, є алгоритмом пошуку.

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

Сенс усього цього полягає в тому, що ви можете поставити будь-який інший алгоритм / клас пошуку, доки він не надає інтерфейс, від якого залежить вектор (пошук бульбашок, послідовний пошук, швидкий пошук).

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

Це економить час (принцип повторного використання коду OOP), оскільки ви зможете написати алгоритм один раз, а не знову і знову і знову конкретно для кожного створеного вами нового об'єкта, не надто ускладнюючи проблему із перерослим деревом спадкування.

Що стосується "пропущення" того, як все працює; великий час (принаймні на 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.

Інтерфейс "Придатний", який реалізують деякі класи, має таку саму вигоду для будь-якої ситуації, коли символи можуть бути додані до екземпляра базового екземпляра об'єкта конкретного класу. Усі ці конкретні класи реалізують придатний інтерфейс:

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


Надто погані інтерфейси, як CharSequenceтакі анемічні. Я хотів би, щоб Java та .NET дозволили інтерфейсам мати реалізацію за замовчуванням, щоб люди не створювали інтерфейси виключно з метою мінімізації кодового коду. З огляду на будь-яку законну CharSequenceреалізацію можна було б імітувати більшість функцій, Stringвикористовуючи лише чотири вищевказані методи, але багато реалізацій могли виконувати ці функції набагато ефективніше іншими способами. На жаль, навіть якщо певна реалізація CharSequenceвміщує все в єдиний char[]і може виконати багато ...
supercat

... такі операції, як indexOfшвидко, немає можливості, щоб абонент, який не знайомий з конкретною реалізацією, CharSequenceміг би попросити це зробити, а не використовувати charAtдля вивчення кожного окремого символу.
supercat

3

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

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

Тому краще оберніть його обкладинкою (в нашій історії це інтерфейс), тоді він прекрасно виконає свою роботу.

Тепер завдання листоноші - отримувати та доставляти лише обкладинки (він би не переймався тим, що знаходиться у обкладинці).

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

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

Я наводжу вам приклад.

у вас є інтерфейс AirPlane, як показано нижче.

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

Припустимо, у вас є такі методи, як у вашому класі Controller of Planes

parkPlane(Airplane plane)

і

servicePlane(Airplane plane)

реалізовані у вашій програмі. Він не порушить ваш код. Я маю на увазі, це не потрібно змінювати, доки він приймає аргументи якAirPlane .

Тому що він буде приймати будь-яку 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 та інших clasess:

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кілька разів,

Припустимо, ваш клас контролера - це 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

Питання: - ... "Чи можете ви використовувати будь-який клас, який реалізує інтерфейс?"
A: - Так.

З: - ... "Коли вам це потрібно було зробити?"
Відповідь: - Щоразу, коли вам потрібні класи, які реалізують інтерфейс (и).

Примітка: нам не вдалося створити інтерфейс, не реалізований класом - True.

  • Чому?
  • Оскільки в інтерфейсі є тільки прототипи методів, а не визначення (просто імена функцій, а не їх логіка)

AnIntf anInst = new Aclass();
// ми могли б це зробити, лише якщо Aclass реалізує AnIntf.
// anInst матиме посилання на Aclass.


Примітка. Тепер ми могли зрозуміти, що сталося, якщо 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 . якщо я, як я вже говорив вище, ви хочете привести роз'єднання від зовнішньої системи (системи зберігання даних) до вашої власної системи (локальної БД), а також використовувати інтерфейс.

зауважте, як існує два способи розглянути, коли їх використовувати. сподіваюся, що це допомагає.


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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.