Чи не вся суть інтерфейсу, що кілька класів дотримуються набору правил та реалізацій?
Чи не вся суть інтерфейсу, що кілька класів дотримуються набору правил та реалізацій?
Відповіді:
Строго кажучи, ні, ви цього не зробите, YAGNI застосовується. Однак, час, який ви витратите на створення інтерфейсу, мінімальний, особливо якщо у вас є зручний інструмент генерації коду, який виконує більшу частину роботи за вас. Якщо ви не впевнені в тому, чи буде вам потрібен інтерфейс чи ні, я б сказав, що краще помилитися з боку щодо підтримки визначення інтерфейсу.
Крім того, використання інтерфейсу навіть для одного класу надасть вам ще одну макетну реалізацію для одиничних тестів - те, що не виробляється. Відповідь Авнера Шахар-Каштана розширюється з цього приводу.
Я відповів би, що вам потрібен інтерфейс чи ні, не залежить від того, скільки класів буде його реалізовувати. Інтерфейси - це інструмент для визначення контрактів між декількома підсистемами вашої програми; тож дійсно важливим є те, як ваша програма розділена на підсистеми. Повинні бути інтерфейси як передній край інкапсульованих підсистем, незалежно від того, скільки класів їх реалізують.
Ось одне дуже корисне правило:
Foo
посилатися безпосередньо на клас BarImpl
, ви рішуче зобов'язуєтесь змінювати Foo
щоразу, коли ви змінюєтесь BarImpl
. Ви в основному трактуєте їх як одну одиницю коду, яка розділена на два класи.Foo
посилаєтесь на інтерфейс Bar
, ви берете на себе зобов'язання, щоб уникнути змін, Foo
коли ви змінюєтесь BarImpl
.Якщо ви визначаєте інтерфейси в ключових точках вашої програми, ви ретельно продумуєте методи, які вони повинні підтримувати, а яким вони не повинні, і ви коментуєте інтерфейси, щоб чітко описати, як має поводитися імплементація (а як ні), Ваша заявка буде набагато легше зрозуміти , тому що ці коментують інтерфейси забезпечують свого роду специфікації в прикладному опис того , як він призначений для себе. Це значно полегшує читання коду (замість того, щоб запитувати "що за чорт цей код повинен робити", ви можете запитати "як цей код робить те, що він повинен робити").
На додаток до всього цього (або власне через це) інтерфейси сприяють окремій компіляції. Оскільки інтерфейси є тривіальними для компіляції та мають меншу кількість залежностей, ніж їх реалізація, це означає, що якщо ви пишете класу Foo
для використання інтерфейсу Bar
, зазвичай його можна перекомпілювати, BarImpl
не потребуючи перекомпіляції Foo
. У великих програмах це може заощадити багато часу.
Foo
залежить від інтерфейсу, Bar
то BarImpl
його можна змінити без необхідності перекомпілювати Foo
. 4. Більш тонкий контроль доступу, ніж публічні / приватні пропозиції (піддайте один клас двом клієнтам через різні інтерфейси).
If you make class Foo refer directly to class BarImpl, you're strongly committing yourself to change Foo every time you change BarImpl
Змінивши BarImpl, які зміни можна уникнути в Foo, використовуючи interface Bar
? Оскільки підписи та функціональність методу не змінюється в BarImpl, Foo не вимагатиме змін навіть без інтерфейсу (вся мета інтерфейсу). Я говорю про сценарій, коли лише один клас, тобто BarImpl
реалізує Bar. У багатокласовому сценарії я розумію принцип інверсії залежності і наскільки корисний інтерфейс.
Інтерфейси призначені для визначення поведінки, тобто набору прототипів функцій / методів. Типи, що реалізують інтерфейс, будуть реалізовувати таку поведінку, тому коли ви маєте справу з таким типом, ви (частково) знаєте, яку поведінку він має.
Не потрібно визначати інтерфейс, якщо ви знаєте, що визначена ним поведінка буде використана лише один раз. KISS (нехай це буде просто, дурно)
Хоча теоретично у вас не повинно бути інтерфейсу тільки заради інтерфейсу, відповідь Яніса Різоса натякає на подальші ускладнення:
Коли ви пишете одиничні тести і використовуєте макетні рамки, такі як Moq або FakeItEasy (щоб назвати два останніх, які я використав), ви неявно створюєте інший клас, який реалізує інтерфейс. Пошук у коді або статичний аналіз може стверджувати, що існує лише одна реалізація, але насправді є внутрішня макетна реалізація. Щоразу, коли ви починаєте писати макети, ви виявите, що витяг інтерфейсів має сенс.
Але зачекайте, є ще більше. Існує більше сценаріїв, коли є неявні реалізації інтерфейсу. Наприклад, використовуючи стек зв'язку .NET WCF, наприклад, генерує проксі для віддаленої служби, яка, знову ж таки, реалізує інтерфейс.
У чистому кодовому середовищі я погоджуюся з рештою відповідей тут. Однак зверніть увагу на будь-які ваші рамки, структури чи залежності, які можуть використовувати інтерфейси.
Ні, вони вам не потрібні, і я вважаю антидіаграмою автоматичне створення інтерфейсів для кожного посилання на клас.
Виробляти Foo / FooImpl для всіх реально варто. IDE може створити інтерфейс / реалізацію безкоштовно, але коли ви переходите до коду, у вас з'являється додаткове когнітивне навантаження від F3 / F12, foo.doSomething()
щоб перейти до підпису інтерфейсу, а не до реальної реалізації, яку ви хочете. Плюс у вас є захаращеність двох файлів замість одного на все.
Тож вам слід робити це лише тоді, коли вам щось потрібно.
Зараз звертаємось до контраргументів:
Мені потрібні інтерфейси для структур вприскування Dependency
Інтерфейси для підтримки фреймворків застарілі. У Java інтерфейси, що раніше були вимогою до динамічних проксі, попередньо CGLIB. Сьогодні вам це зазвичай не потрібно. Прогрес і користь для продуктивності розробників вам більше не потрібні в EJB3, Spring тощо.
Мені потрібні макети для тестування одиниць
Якщо ви пишете власні макети та маєте дві фактичні реалізації, то інтерфейс підходить. Ми, мабуть, не мали б цього обговорення в першу чергу, якби у вашій кодовій базі були і FooImpl, і TestFoo.
Але якщо ви використовуєте глузуючі рамки, такі як Moq, EasyMock або Mockito, ви можете знущатися над класами, і вам не потрібні інтерфейси. Це аналогічно налаштуванню foo.method = mockImplementation
в динамічній мові, де можна призначити методи.
Нам потрібні інтерфейси для дотримання принципу інверсії залежності (DIP)
DIP говорить, що ви будуєте, щоб залежати від контрактів (інтерфейсів), а не від реалізації. Але клас - це вже договір і абстракція. Ось для чого і публічні / приватні ключові слова. В університеті канонічним прикладом було щось на кшталт матричного чи поліномального класу - споживачі мають публічний API для створення матриць, додавання їх тощо, але їм заборонено дбати, чи матриця реалізована у розрідженій чи щільній формі. Для доказу цього пункту не було потрібних IMatrix або MatrixImpl.
Крім того, DIP часто надмірно застосовується на кожному рівні виклику класу / методу, а не тільки на основних меж модуля. Ознакою того, що ви надто застосовуєте DIP, є те, що ваш інтерфейс та реалізація змінюються на крок блокування таким чином, що вам потрібно торкнутися двох файлів, щоб внести зміни замість одного. Якщо DIP застосовано належним чином, це означає, що ваш інтерфейс не повинен часто змінюватися. Також ще одна ознака - ваш інтерфейс має лише одного реального споживача (власне додаток). Інша історія, якщо ви будуєте бібліотеку класів для споживання у багатьох різних додатках.
Це є наслідком для думки дядька Боба Мартіна щодо глузування - вам слід лише знущатися над основними архітектурними межами. У веб-програмі основними межами є доступ до HTTP та БД. Усі дзвінки класу / методу між ними не є. Те саме стосується і DIP.
Дивитися також:
new Mockup<YourClass>() {}
і весь клас, включаючи його конструктори, знущаються, будь то інтерфейс, абстрактний клас чи конкретний клас. Ви також можете "перекрити" поведінку конструктора, якщо бажаєте цього зробити. Я припускаю, що в Mockito або Powermock є рівнозначні способи.
Схоже, відповіді з обох боків огорожі можна підсумувати так:
Добре спроектуйте і поставте інтерфейси там, де потрібні інтерфейси.
Як я зазначив у своїй відповіді на відповідь Янні , я не думаю, що ви коли-небудь можете мати жорстке і швидке правило щодо інтерфейсів. Правило, за визначенням, має бути гнучким. Моє правило щодо інтерфейсів полягає в тому, що інтерфейс повинен використовуватися в будь-якому місці, де ви створюєте API. І API слід створити в будь-якому місці, де ви перетинаєте межу з однієї сфери відповідальності в іншу.
На прикладі (жахливо надуманого) припустімо, що ви будуєте Car
клас. У вашому класі вам неодмінно знадобиться шар інтерфейсу. У цьому конкретному прикладі, він приймає форму IginitionSwitch
, SteeringWheel
, GearShift
, GasPedal
, і BrakePedal
. Оскільки цей автомобіль містить AutomaticTransmission
, вам не потрібно ClutchPedal
. (А оскільки це жахливий автомобіль, немає А / С, радіо чи сидіння. Насправді, профнастилу теж не вистачає - потрібно просто повіситись на це кермо і сподіватися на найкраще!)
Тож якому з цих класів потрібен інтерфейс? Відповідь може бути всі вони, або жодна з них - залежно від вашого дизайну.
Ви можете мати інтерфейс, який виглядав приблизно так:
Interface ICabin
Event IgnitionSwitchTurnedOn()
Event IgnitionSwitchTurnedOff()
Event BrakePedalPositionChanged(int percent)
Event GasPedalPositionChanged(int percent)
Event GearShiftGearChanged(int gearNum)
Event SteeringWheelTurned(float degree)
End Interface
З цього моменту поведінка цих класів стає частиною інтерфейсу / API інтерфейсу ICabin. У цьому прикладі класи (якщо такі є), ймовірно, прості, з кількома властивостями та функцією чи двома. І те, що ви неявно заявляєте у своєму дизайні, - це те, що ці класи існують виключно для того, щоб підтримувати будь-яку конкретну реалізацію ICabin у вас є, і вони не можуть існувати самостійно, або вони безглузді поза контекстом ICabin.
Це та сама причина, що ви не використовуєте тестування приватних членів - вони існують лише для підтримки загальнодоступного API, і тому їх поведінку слід перевірити шляхом тестування API.
Так що, якщо ваш клас існує виключно для підтримки іншого класу, і концептуально переглядати це не на самому ділі маючи свій власний домен , то це нормально , щоб пропустити інтерфейс. Але якщо ваш клас достатньо важливий, що ви вважаєте його досить дорослим, щоб мати власний домен, тоді продовжуйте і надайте йому інтерфейс.
Редагувати:
Часто (включаючи цю відповідь) ви читаєте такі речі, як "домен", "залежність" (часто поєднані з "ін'єкцією"), які не означають нічого для вас, коли ви починаєте програмувати (вони впевнені, що не зробили значить щось для мене). Для домену це означає саме те, що воно звучить:
Територія, над якою здійснюється панування чи влада; володіння суверена або співдружності тощо. Також використовується образно. [WordNet smislu 2] [1913 Вебстер]
З точки зору мого прикладу - розглянемо IgnitionSwitch
. У машині м'ясного простору перемикач запалювання відповідає за:
Ці властивості складають домен того IgnitionSwitch
, про що він знає і за що відповідає.
The IgnitionSwitch
не несе відповідальності за GasPedal
. Перемикач запалювання абсолютно не знає педалі газу. Вони обидва працюють абсолютно незалежно одне від одного (хоча автомобіль був би бездоганним без обох!).
Як я спочатку зазначив, це залежить від вашого дизайну. Ви можете спроектувати зображення, IgnitionSwitch
яке має два значення: On ( True
) та Off ( False
). Або ви можете спроектувати його для автентифікації наданого для нього ключа та безлічі інших дій. Це складна частина того, щоб бути розробником, вирішуючи, де намалювати лінії на піску - і, чесно кажучи, більшість часу це абсолютно відносно. Ці рядки в піску є важливими - саме там знаходиться ваш API, а значить, там, де мають бути ваші інтерфейси.
Від MSDN :
Інтерфейси краще підходять для ситуацій, коли ваші програми потребують багатьох можливо непов'язаних типів об'єктів, щоб забезпечити певну функціональність.
Інтерфейси є більш гнучкими, ніж базові класи, тому що ви можете визначити єдину реалізацію, яка може реалізувати кілька інтерфейсів.
Інтерфейси краще в тих ситуаціях, коли вам не потрібно успадковувати реалізацію від базового класу.
Інтерфейси корисні у випадках, коли ви не можете використовувати успадкування класів. Наприклад, структури не можуть успадкувати класи, але вони можуть реалізувати інтерфейси.
Як правило, у випадку з одним класом не потрібно буде впроваджувати інтерфейс, але, враховуючи майбутнє вашого проекту, може бути корисним формально визначити необхідну поведінку класів.
Щоб відповісти на питання: Є в цьому більше, ніж це.
Одним з важливих аспектів інтерфейсу є намір.
Інтерфейс - це "абстрактний тип, який не містить даних, але розкриває поведінку" - інтерфейс (обчислення). Отже, якщо це поведінка або набір поведінки, який підтримує клас, ніж, швидше за все, інтерфейс є правильною схемою. Якщо ж поведінка (и) є (є) суттєвою концепцією, втіленою класом, то вам, швидше за все, інтерфейс взагалі не потрібен.
Перше питання, яке потрібно задати - це природа речі чи процесу, яку ви намагаєтесь представляти. Потім перейдіть до практичних причин здійснення даної природи певним чином.
Оскільки ви задали це питання, я вважаю, що ви вже бачили інтереси мати інтерфейс, який приховує кілька реалізацій. Це може проявлятися за принципом інверсії залежності.
Однак необхідність мати інтерфейс чи ні не залежить від кількості його реалізацій. Справжня роль інтерфейсу полягає в тому, що він визначає договір із зазначенням того, яку послугу слід надавати замість того, як вона має бути реалізована.
Після визначення договору дві або більше команд можуть працювати самостійно. Скажімо, ви працюєте над модулем A, і це залежить від модуля B, факт створення інтерфейсу на B дозволяє продовжувати вашу роботу, не турбуючись про реалізацію B, оскільки всі деталі приховані інтерфейсом. Таким чином, розподілене програмування стає можливим.
Хоча модуль B має лише одну реалізацію свого інтерфейсу, інтерфейс все ще необхідний.
На закінчення інтерфейс приховує деталі реалізації від своїх користувачів. Програмування на інтерфейс допомагає писати більше документів, оскільки контракт повинен бути визначений, писати більш модульне програмне забезпечення, сприяти тестуванню одиниць та прискорити швидкість розробки.
Усі відповіді тут дуже хороші. Адже більшу частину часу вам не потрібно впроваджувати інший інтерфейс. Але є випадок , коли ви можете хотіти зробити це в будь-якому випадку. Ось випадок, коли я це роблю:
Клас реалізує інший інтерфейс, який я не хочу виставляти
часто, з класом адаптерів, який з'єднує код сторонніх розробників.
interface NameChangeListener { // Implemented by a lot of people
void nameChanged(String name);
}
interface NameChangeCount { // Only implemented by my class
int getCount();
}
class NameChangeCounter implements NameChangeListener, NameChangeCount {
...
}
class SomeUserInterface {
private NameChangeCount currentCount; // Will never know that you can change the counter
}
Клас використовує специфічну технологію, яка не повинна просочуватися
здебільшого під час взаємодії із зовнішніми бібліотеками. Навіть якщо є лише одна реалізація, я використовую інтерфейс для того, щоб не вводити непотрібне з'єднання із зовнішньою бібліотекою.
interface SomeRepository { // Guarantee that the external library details won't leak trough
...
}
class OracleSomeRepository implements SomeRepository {
... // Oracle prefix allow us to quickly know what is going on in this class
}
Перехресне спілкування
Навіть якщо лише один клас інтерфейсу реалізує один із класів домену, це дозволяє краще розділити між цим шаром, а головне - уникнути циклічної залежності.
package project.domain;
interface UserRequestSource {
public UserRequest getLastRequest();
}
class UserBehaviorAnalyser {
private UserRequestSource requestSource;
}
package project.ui;
class OrderCompleteDialog extends SomeUIClass implements project.domain.UserRequestSource {
// UI concern, no need for my domain object to know about this method.
public void displayLabelInErrorMode();
// They most certainly need to know about *that* though
public UserRequest getLastRequest();
}
Для більшості об'єктів має бути доступний лише підмножина методу.
Здебільшого це відбувається, коли у мене є певний метод конфігурації для конкретного класу
interface Sender {
void sendMessage(Message message)
}
class PacketSender implements Sender {
void sendMessage(Message message);
void setPacketSize(int sizeInByte);
}
class Throttler { // This class need to have full access to the object
private PacketSender sender;
public useLowNetworkUsageMode() {
sender.setPacketSize(LOW_PACKET_SIZE);
sender.sendMessage(new NotifyLowNetworkUsageMessage());
... // Other details
}
}
class MailOrder { // Not this one though
private Sender sender;
}
Отже, врешті-решт, я використовую інтерфейс з тієї ж причини, що і я використовую приватне поле: інший об’єкт не повинен мати доступу до речей, до яких вони не мають доступу. Якщо у мене є такий випадок, я впроваджую інтерфейс, навіть якщо реалізує його лише один клас.
Інтерфейси дійсно важливі, але постарайтеся контролювати їх кількість.
Відправившись по створенню інтерфейсів майже на все, легко в кінцевому підсумку з кодом "подрібнених спагетті". Я відкладаю більшу мудрість Айенде Рахієн, яка опублікувала кілька дуже мудрих слів на цю тему:
http://ayende.com/blog/153889/limit-your-abstractions-analyzing-a-ddd-application
Це його перший пост у цілій серії, тому продовжуйте читати!
Однією з причин, по якій ви все ще можете ввести інтерфейс у цьому випадку, є дотримання принципу інверсії залежності . Тобто модуль, який використовує клас, буде залежати від абстрагування його (тобто інтерфейсу), а не від конкретної реалізації. Він відокремлює компоненти високого рівня від компонентів низького рівня.
Немає реальної причини щось робити. Інтерфейси - це допомогти вам, а не вихідній програмі. Тож навіть якщо Інтерфейс реалізований мільйонами класів, немає жодного правила, яке говорить про те, що його потрібно створити. Ви створюєте його так, що коли ви чи хтось інший, хто використовує ваш код, хочуть змінити щось, що стосується всіх реалізацій. Створення інтерфейсу допоможе вам у всіх майбутніх випадках, коли ви можете створити інший клас, який його реалізує.
Не завжди потрібно визначати інтерфейс для класу.
Прості об'єкти, такі як об'єкти цінності, не мають декількох реалізацій. Їм теж не потрібно глузувати. Реалізація може бути протестована самостійно, і коли тестуються інші класи, які залежать від них, може бути використаний фактичний об'єкт значення.
Пам'ятайте, що створення інтерфейсу має вартість. Його потрібно оновлювати в ході реалізації, йому потрібен додатковий файл, і деякі IDE матимуть проблеми із збільшенням масштабу в реалізації, а не інтерфейсом.
Тож я б визначив інтерфейси лише для класів вищого рівня, де потрібно абстрагуватися від реалізації.
Зауважте, що з класом ви отримуєте інтерфейс безкоштовно. Окрім реалізації, клас визначає інтерфейс із набору загальнодоступних методів. Цей інтерфейс реалізований усіма похідними класами. Це не строго кажучи інтерфейс, але його можна використовувати точно так само. Тому я не думаю, що потрібно відтворювати інтерфейс, який вже існує під назвою класу.