Чи існує принцип інтерфейсу "запитувати лише те, що вам потрібно"?


9

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

Наприклад, якщо у мене є купа типів, які можна видалити, я зроблю Deletableінтерфейс:

interface Deletable {
   void delete();
}

Тоді я можу написати загальний клас:

class Deleter<T extends Deletable> {
   void delete(T t) {
      t.delete();
   }
}

В іншому випадку я завжди буду просити про найменшу відповідальність за виконання потреб клієнтського коду. Тож якщо мені потрібно лише видалити a File, я все одно прошу a Deletable, а не a File.

Цей принцип є загальновідомим і вже має прийняту назву? Це суперечливо? Чи обговорюється це в підручниках?


1
Можливо, вільна муфта? Або вузькі інтерфейси?
tdammers

Відповіді:


16

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


4

Щоб розкрити дуже гарну відповідь Вадима, я відповім на питання "чи це спірне" з "ні, не дуже".

Загалом, розділення інтерфейсів - це гарна річ, зменшуючи загальну кількість "причин зміни" різних об'єктів, що беруть участь. Основний принцип полягає в тому, що коли інтерфейс з декількома методами потрібно змінити, скажімо, щоб додати параметр до одного з методів інтерфейсу, тоді всі споживачі інтерфейсу повинні бути принаймні перекомпільовані, навіть якщо вони не використовували метод, який змінився. "Але це просто перекомпіляція!", Я чую, як ви говорите; це може бути правдою, але майте на увазі, що зазвичай все, що ви перекомпілюєте, повинно бути витіснене як частина програмного патчу, незалежно від того, наскільки суттєві зміни в двійковому. Ці правила були спочатку осмислені ще на початку 90-х, коли середня робоча станція на робочому столі була менш потужною, ніж телефон у вашій кишені, набір 14,4 тис. Бод був блазним, а 3,5-дюймові "дискети" 1,44 МБ були основним знімним носієм. Навіть у сучасну епоху 3G / 4G користувачі бездротового Інтернету часто мають плани даних з обмеженнями, тож, випускаючи оновлення, тим менше бінарних файлів, які потрібно завантажити, тим краще.

Однак, як і всі хороші ідеї, розмежування інтерфейсу може погіршитись, якщо неправильно реалізовано. По-перше, існує ймовірність, що, розділяючи інтерфейси, зберігаючи об'єкт, який реалізує ці інтерфейси (виконуючи залежності), відносно незмінними, ви можете опинитися з "Hydra", родичем антитіл "Об'єкт Бога", де всезнаючий, всесильний характер об'єкта прихований від залежних вузькими інтерфейсами. У вас опинилося багатоголове чудовисько, яке, як мінімум, так важко підтримувати, як і об'єкт Бога, плюс накладні витрати на підтримку всіх його інтерфейсів. Не існує важкої кількості інтерфейсів, яку ви не повинні перевищувати, але кожен інтерфейс, який ви реалізуєте на одному об’єкті, повинен бути попереднім, відповівши на питання: "Чи сприяє цей інтерфейс об'єкту"

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

Сегрегація інтерфейсу повинна базуватися на класах, які залежать від інтерфейсу:

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

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

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


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

Параметри загального типу можна визначити за допомогою критеріїв, включаючи реалізацію декількох інтерфейсів, але ви праві, що вирази, що вимагають статичного типу, зазвичай не можуть підтримувати вказівку більше одного. Якщо є необхідність, щоб статичний тип реалізував як IFoo, так і IBar, а ви керували обома цими інтерфейсами, можливо, буде корисною реалізацією IBaz : IFoo, IBarта вимагати цього.
KeithS

Якщо клієнтському коду може знадобитися щось, що може бути використано як IFooі IBar, то визначення композиту IFooBarможе бути непоганою ідеєю, але якщо інтерфейси дрібно розбиті, це в кінцевому підсумку вимагає десятків різних типів інтерфейсу. Розглянемо такі колекції можливостей: Перерахувати, Порахувати звіт, Прочитати n-й елемент, Записати n-й елемент, Вставити перед n-м елементом, Видалити n-й елемент, Новий елемент (збільшити збір і повернути індекс нового простору) та Додати. Дев'ять методів: ECRWIDNA. Я, мабуть, міг описати десятки типів, які, природно, підтримували б багато різних комбінацій.
supercat

Наприклад, масиви підтримували б ECRW. Арраїст підтримав ECRWIDNA. Список, захищений від потоку, може підтримувати ECRWNA [хоча A, як правило, корисний лише для попереднього заповнення списку]. Обгортка масиву лише для читання може підтримувати ECR. Інтерфейс коваріантного списку може підтримувати ECRD. Негенеричний інтерфейс може забезпечити безпечну підтримку типу C або CD. Якщо Swap був варіантом, деякі типи можуть підтримувати CS, але не D (наприклад, масиви), а інші підтримують CDS. Спроба визначити різні типи інтерфейсу для кожної необхідної комбінації здібностей була б кошмаром.
supercat

Тепер уявіть, що хочеться вміти обгортати колекцію об'єктом, який може робити все, що може зробити колекція, але який реєструє кожну транзакцію. Скільки фантиків знадобиться? Якби всі колекції успадковувались із загального інтерфейсу, який включав властивості для виявлення їх здібностей, вистачить однієї обгортки. Однак, якщо всі інтерфейси відрізняються, потрібні були б десятки.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.