Все, що ви хотіли знати про принципи SOLID. Частина четверта: ISP
Привіт! З вами знову Сергій Немчинський. За більш ніж двадцять років роботи в ІТ я поставив собі мету: робити так, щоб навколо мене було більше хороших програмістів. Тому я займаюся викладацтвом та формуванням потрібного мейндсету в колег.
Частина моєї просвітницької місії — популяризація принципів об’єктноорієнтованого програмування, відомих як SOLID. Їх часто критикують за розмитість, нерелевантність тощо. Але знаєте, таке буває навіть із правилами розстановки ком у реченні. Тому домовимося: перш ніж щось критикувати, спочатку зрозуміємо, навіщо його взагалі вигадали та впровадили.
В попередніх частинах
У перших трьох статтях циклу я розповідав, що принципи SOLID зібрав відомий програміст Роберт Мартін, він же Дядько Боб. Кожна літера в цьому слові — початкова для назви одного з п’яти принципів. Їх, звісно, більше, але саме перша п’ятірка лягла в основу концепції «чистого коду».
Single Responsibility Principle — кожен клас повинен мати лише одну причину для змін.
Open Closed Principle — код має бути відкритим до додавання нового функціоналу, але все, що вже написано, не повинно змінюватися.
Liskov Substitution Principle — поведінка похідних класів не повинна суперечити поведінці, заданій базовим класом.
Час переходити до четвертого принципу — Interface Segregation Principle, принципу розділення інтерфейсів.
Принцип розділення інтерфейсів: формулювання та ідея
Interface Segregation Principle (ISP) стверджує: «Клієнти не повинні залежати від методів, які вони не використовують». Тобто якщо метод інтерфейсу не використовується клієнтом, то зміни цього методу не повинні призводити до внесення змін у клієнтський код.
Основна ідея ISP: інтерфейс є власністю не сервера, а клієнта. Інтерфейс повинен бути сформульований у термінах клієнта і містити лише ті методи, які клієнтові потрібні.
Проблема «товстих» інтерфейсів
Розглянемо типову ситуацію. У вас є фасад якоїсь великої системи з величезною кількістю методів. Ви витягуєте всі методи фасаду в один інтерфейс і працюєте з системою через цей інтерфейс.
Але у системи є різні клієнти, яким потрібні різні методи.
- Клієнт А використовує лише методи 1 і 2.
- Клієнт Б використовує методи
3-8. - Клієнт В використовує лише метод 10.
До речі, з’ясувалося, що метод 9 взагалі нікому не потрібен, але це окреме питання.
Використання єдиного інтерфейсу для всіх клієнтів створює певні проблеми.
- Непотрібні залежності. Якщо змінюється сигнатура одного методу — наприклад, 10, всі клієнти мають перекомпілюватися, навіть ті, що його не використовують.
- Складність моків. Кожен клієнт має створювати мок-реалізації для всіх методів, навіть непотрібних.
- Складність заміни. При заміні фасаду на інший доводиться переписувати весь код.
Правильне рішення згідно ISP — створити власний інтерфейс для кожного клієнта.

Як це може виглядати на практиці: ось у вас є база даних, а в ній користувачі з різним рівнем доступу і різними правами. Можна зробити єдиний інтерфейс для всіх користувачів, а потім вирішувати, як заблокувати звичайним юзерам можливість створювати та видаляти записи. А можна зробити різні інтерфейси:
- ReadOnlyInterface (для звичайних користувачів) — лише пошук.
- FullAccessInterface (для адміністраторів) — всі функції.
Таким чином зменшиться кількість роботи, можливих помилок та ентропії Всесвіту.
Тонке питання проєктування інтерфейсів
Ви скажете — а як визначити, скільки потрібно клієнтів, який інтерфейс має бути в кожного, як методи мають використовуватися, і найголовніше — хто за це відповідатиме? Тому що часто-густо бекенд-розробники звертаються до колег-фронтендників по інтерфейси, а ті кажуть: «Які вам треба? Дайте нам хоч щось».
Я вважаю, що проблема інтерфейсів — це проблема фронтендерів. Це вони працюють з новими трендами UI, а також із сучасними фронтенд-фреймворками, такими як React, Vue, Angular, які передбачають роботу з невеликими компонентами. Це вони знають, які платформи та пристрої використовують юзери.
І найголовніше — це інтерфейс має ходити за клієнтом, а не навпаки. Тому ось мій заповіт бекендникам: якщо ваші фронтенди просять дати інтерфейси, натомість дайте їм по голові. Скажіть, Немчинський дозволив.
ISP для фронтенду — не просто принцип гарного коду, це практична необхідність для створення масштабованих, підтримуваних застосунків.
Зв’язок з іншими принципами SOLID
Interface Segregation Principle (ISP) має глибокі зв’язки з іншими принципами SOLID та концепціями об’єктноорієнтованого програмування.
Single Responsibility Principle (SRP)
SRP і ISP доповнюють один одного. SRP фокусується на тому, щоб в класі знаходились лише подібні похідні. ISP працює над тим, щоб інтерфейс використовував лише потрібні методи. Тобто обидва методи направлені на те, щоб не використовувати зайві ресурси.
High Cohesion
Цей принцип не відноситься до SOLID і з’явився значно раніше за об’єктноорієнтоване програмування. High Cohesion означає, що елементи всередині модуля (класу, інтерфейсу) повинні бути тісно пов’язані функціонально. Дуже подібне до SRP та його цілей.
Open/Closed Principle (OCP)
ISP допомагає дотримуватися OCP, дозволяючи легко розширювати функціональність без модифікації існуючого коду.
Dependency Inversion Principle (DIP)
Цей принцип останній з принципів SOLID, його ми розглянемо в наступній статті. ISP і DIP разом створюють гнучку архітектуру: замість залежності від конкретного класу з великим інтерфейсом використовуються специфічні інтерфейси.
Практичне застосування ISP
Продовжуємо наш міні-серіал про програмістів Петра, Василя та датчики. Ви пишете, що слова «датчик» в українській мові немає. Але, по-перше, ані словники, ані Вікіпедія з вами незгодні. По-друге, якщо зараз я заміню датчики на давачі, не буде зрозуміло, чи йдеться про ті самі девайси. Схоже на порушення принципів SOLID, так? Тому залишимо як є.
Нова проблема в системі датчиків
Наші герої Петро і Василь працюють над розробкою Embedded-системи, в якій є датчик, що вимірює час та температуру. Проєкт успішно еволюціонує, команда росте. І ось нова проблема: нові розробники скаржаться, що інтерфейс Sensor занадто складний для використання.
Петро, який вже доріс до тімліда, починає розбиратися і дивиться код інтерфейсу. Дійсно, складно. Аж дванадцять методів. А чи потрібні всі ці методи кожному, хто використовує датчики?
Швидкий аналіз показує, що є три основні групи користувачів системи: оператори, технічні спеціалісти та системні адміністратори.
Операторам, на яких прийшлося 90% використання системи, потрібні лише два методи:
- getValue() — отримати показання;
- getUnit() — дізнатися одиниці вимірювання.
Технічним спеціалістам, окрім двох базових методів, потрібні ще три:
- calibrate() — калібрувати датчики;
- getDiagnostics() — діагностика;
- performSelfTest() — самотестування.
І лише системним адміністраторам (2% використання системи) потрібний повний доступ до всіх методів, включаючи:
- updateFirmware() — оновлення прошивки;
- resetToFactory() — скидання до заводських налаштувань;
- setMaintenanceMode() — режим обслуговування.
Аналіз дав розуміння, чому інтерфейс здається складним, а ще — яким чином один з операторів нещодавно скинув налаштування до заводських.
Тому Петро поставив задачу Василеві розділити інтерфейси за принципом ISP. Їх зробили три: базовий, розширений та повний, кожен з яких був розроблений для своєї групи користувачів.

Тут ми ще могли б вигадати операторку Марію, розробника Олексія та сісадміна Івана, які всі виграли від впровадження нових інтерфейсів. Але давайте просто підсумуємо вигоди від застосування принципу ISP.
- Спрощення для операторів. Вони бачать лише ті методи, які їм потрібні.
- Більша безпека. Користувачі не можуть випадково викликати щось не те.
- Швидше тестування. Замість 12 методів тестуються лише 2 базові. Тести пишуться у 5 разів швидше.
- Легкість реалізації нового функціоналу. Можна створювати датчики з новими функціями та поетапним розширенням функціоналу.
Як бачите, від впровадження принципу ISP — самі тільки бонуси.
Коли можна не використовувати ISP
Це ваша улюблена частина, де я розповідаю, як не використовувати те, що варто було б. Але навіть ISP не є догмою. В деяких ситуаціях його застосування може бути недоцільним або навіть шкідливим. Ось декілька прикладів.
- Якщо інтерфейс давно існує, добре протестований і рідко змінюється, його розділення може принести більше проблем, ніж користі.
- У маленьких проєктах з невеликою командою надмірне розділення може створити зайву складність.
- Коли ви тестуєте ідеї, передчасне розділення може уповільнити розробку.
- Якщо методи інтерфейсу тісно пов’язані між собою і логічно утворюють єдине ціле, їх розділення може зашкодити розумінню коду.
Головне правило: ISP — це інструмент для вирішення конкретних проблем, а не самоціль.
Висновок та практичні поради
Interface Segregation Principle — це потужний інструмент для створення чистої архітектури. Пам’ятайте головне правило: інтерфейс належить клієнту, а не серверу. Формулюйте інтерфейси виходячи з потреб клієнта, а не можливостей сервера.
Це особливо важливо у великих проєктах, де різні частини системи мають різні потреби та вимоги.
А ось і поради.
Для Back-end-розробників: вимагайте від Front-end-розробників чіткого визначення потрібних їм інтерфейсів. Не намагайтеся вгадати, що їм потрібно — нехай вони самі формулюють свої вимоги.
Для проєктувальників API: створюйте кілька вузькоспеціалізованих ендпоінтів замість одного універсального, який повертає зайву інформацію.
Для системних архітекторів: розділяйте інтерфейси за зонами використання — внутрішні (для використання в пакеті) та зовнішні (для публічного API).
Для всіх: вам не завжди подобатиметься апеляція до здорового глузду, але в програмуванні без нього ніяк — це вам не життя. Тому пам’ятайте, що жоден принцип не є догмою, включаючи ISP, і використовуйте їх розважливо.
7 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівSOLID TypeScript: github.com/...erns-and-SOLID-Principles
@Override public void add(int index, E element) { throw uoe(); } @Override public boolean addAll(int index, Collection<? extends E> c) { throw uoe(); } @Override public E remove(int index) { throw uoe(); } @Override public void replaceAll(UnaryOperator<E> operator) { throw uoe(); } @Override public E set(int index, E element) { throw uoe(); } @Override public void sort(Comparator<? super E> c) { throw uoe(); }— якщо ISP такий важливий — чому це не було зроблено у JRE?— якщо це не важливо для JRE — чому взагалі варто цим перейматись?
Ну, скажемо так — до JRE взагалі багато питань, але всі вони мають одну відповідь: так історічно склалося і треба підтримувати зворотню сумісність
Чесно кажучи, не дуже люблю теоретичні балачки без прикладів коду, але залишу коментар.
Щось ви, дядьку, не те розказуєте.
По-перше, ви, здається, плутаєте interface та API.
Чого б це?
Чому це мають перекомпліватися?
Це якщо руками їх створювати. А якщо, наприклад, за допомогою Mockito — то ні.
До чого тут фронтендери? Ми про API чи про ISP?
Я розумію ISP приблизно так само як в статті на Baeldung пояснено.
Не треба робити interface на парканадцять методів, бо тоді в
class SomeClass implements BigInterface { ... }... їх тре буде всіх імплементувати. Навіть якщо вони релеванті тільки для специфічних кейсів. І це дійсно не ок.
Ось і все. Як проектувати API між фронтендом та бекендом — це геть інша історія.
Я використовую слово «інтерфейс» як загальну назву для всіх випадків — і для інтерфейса на рівні мови і для АРІ і для фасадів і навіть для ендпойнтів. Просто для всього, що служить як точка доступу ззовні, будь то іншій шматок коду або інша система
пишіть «сенсор», та й усе
як варіант, але що робити з попередніми статями? Як же зворотня сумісність? :)