ISP (Interface Segregation Principle) — четвертий принцип SOLID, який стверджує: клієнти не повинні залежати від методів, які вони не використовують. Принцип був сформульований Робертом Мартіном у контексті проектування інтерфейсів для об'єктно-орієнтованих систем. Як описано в книзі Clean Architecture (2017), принцип розділення інтерфейсів вимагає створювати вузькоспеціалізовані інтерфейси замість одного універсального, що зменшує зв'язність і спрощує внесення змін.
Головне
ISP (Interface Segregation Principle) — принцип розділення інтерфейсів, який забороняє створювати <<товсті>> інтерфейси з методами, які не використовуються всіма клієнтами. Замість одного інтерфейсу з десятком методів проектуються кілька маленьких інтерфейсів, кожен для своєї групи клієнтів.
Принцип був введений Робертом Мартіном як вирішення проблеми <<забруднення інтерфейсів>>, коли один клас змушений реалізовувати методи, які йому не потрібні, лише тому що вони оголошені в загальному інтерфейсі. У статично типізованих мовах це виливається в порожні реалізації або викидання винятків — пряма ознака порушення ISP.
ISP і SRP доповнюють один одного: SRP про відповідальність класу, ISP про контракти інтерфейсів. SRP говорить <<один клас — одна причина для зміни>>, ISP — <<один інтерфейс — один клієнтський сценарій>>. Разом вони формують модульну архітектуру, де кожен елемент системи має чіткі межі.
Fat Interface — інтерфейс, що містить більше методів, ніж потрібно конкретному клієнту. Наприклад, інтерфейс Worker з методами work, eat, sleep. Робот-робітник не повинен реалізовувати eat і sleep, але змушений. Рішення — розділити на Workable, Eatable, Sleepable. Кожен клієнт отримує саме те, що йому потрібно.
У мобільній розробці товсті інтерфейси зустрічаються в протоколах делегатів і DataSource. Один протокол може містити методи для двох різних сценаріїв (редагування + відображення), хоча конкретний екран використовує лише один з них.
Реалізація ISP починається з аналізу клієнтів кожного інтерфейсу. Якщо два клієнти використовують різні набори методів одного інтерфейсу — інтерфейс слід розділити. Кожен новий інтерфейс групує методи, які викликаються разом у рамках одного сценарію.
Механізм розділення: вихідний інтерфейс розбивається на кілька вузьких, кожен з яких успадковує спільну частину (якщо вона є). Клієнти перемикаються на залежність від потрібного вузького інтерфейсу замість загального. Класи, що реалізують вихідний інтерфейс, тепер реалізують лише ті вузькі інтерфейси, які їм дійсно потрібні.
Важливе уточнення: ступінь дроблення визначається кількістю клієнтів та їх сценаріїв. ISP не вимагає максимального дроблення (мікроінтерфейси по одному методу). Це призвело б до надмірної складності. Мета — усунути залежність клієнтів від непотрібних методів, а не мінімізувати розмір кожного інтерфейсу.
Основні ознаки порушення ISP включають: класи, що реалізують інтерфейс з порожніми методами (фіктивна реалізація), викидання винятку UnsupportedOperationException у реалізаціях, велика кількість параметрів або типів, що повертаються, які не використовуються частиною клієнтів, і часті зміни інтерфейсу, що зачіпають лише частину клієнтів.
У Android-розробці типовий приклад порушення ISP — інтерфейс OnItemClickListener, який включає методи для кліку, довгого кліку та свайпу. Якщо конкретний екран використовує лише клік — решта методів залишаються порожніми. Рішення — розділити на OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener.
У iOS-розробці порушення ISP проявляється в делегатах UIKit: один протокол містить методи для різних станів компонента. UITableViewDelegate включає методи для відображення, виділення, редагування та swipe-action. Часто розробники реалізують протокол цілком з десятком порожніх методів. Розділення на кілька протоколів за групами відповідальності вирішує проблему.
Проблема не лише в естетиці коду. Коли інтерфейс змінюється (додається новий метод), всі класи-реалізатори повинні бути оновлені — навіть ті, кому новий метод не потрібен. У мобільній розробці з десятками екранів це призводить до каскадних змін. ISP ізолює кожен клієнт від змін, які його не стосуються.
Неявне порушення ISP відбувається через параметри конфігурації. Якщо метод приймає об'єкт з великою кількістю полів, а клієнт використовує лише 2-3 з них — це сигнал до дроблення. Альтернатива: кілька спеціалізованих методів з мінімальним набором параметрів.
У Android-розробці ISP порушується при використанні одного SharedPreferencesManager для читання та запису всіх налаштувань додатка. Fragment, якому потрібно лише прочитати тему, отримує залежність від глобального менеджера з десятком методів для різних типів даних. Розділення на ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider — застосування ISP на рівні сервісів конфігурації. Кожен провайдер містить саме ті методи, які потрібні його клієнтам.
Розглянемо Android-приклад з інтерфейсом для роботи з даними. Порушення ISP — один інтерфейс для всіх CRUD операцій, хоча не всім клієнтам потрібні всі операції.
// Порушення ISP: товстий інтерфейс
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// Після застосування ISP: вузькі інтерфейси
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// ReadOnlyViewModel не залежить від методів запису
class ReadOnlyViewModel(
private val reader: UserReader
)
iOS-приклад з розділенням протоколів для роботи з медіа:
// Порушення ISP: один протокол для всієї роботи з медіа
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// Після ISP: розділення на протоколи за відповідальністю
protocol MediaPlayer {
func play(url: URL)
func pause()
func stop()
}
protocol MediaTransfer {
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// PlayerViewModel не залежить від методів завантаження
class PlayerViewModel {
private let player: MediaPlayer
}
Практичний висновок: ISP захищає клієнтів від змін у незалежних від них частинах інтерфейсу. Розділення UserRepository на UserReader і UserWriter означає, що зміни в save не зачіпають ReadOnlyViewModel, і навпаки. Кожен клієнт ізольований від функціональності, яку не використовує, і не потребує змін при доопрацюванні інших частин системи.
ISP і SRP — природна пара. SRP визначає, що клас повинен мати одну причину для зміни. ISP застосовує ту саму логіку до інтерфейсів: інтерфейс повинен обслуговувати один клієнтський сценарій. Клас може реалізовувати кілька вузьких інтерфейсів (кожен відповідає одній відповідальності), що чистіше, ніж один товстий інтерфейс з кількома відповідальностями.
ISP і OCP також пов'язані: вузькі інтерфейси легше розширювати. Додавання нового методу у вузький інтерфейс зачіпає лише його клієнтів. Додавання методу в товстий інтерфейс зачіпає всіх клієнтів — потенційно порушуючи OCP, якщо клієнти змушені змінювати свою реалізацію.
ISP і DIP працюють разом: DIP вимагає залежності від абстракцій. ISP робить ці абстракції вузькими та сфокусованими. Залежність від широкого інтерфейсу — це все ще залежність від абстракції, але <<поганої>> абстракції з точки зору ISP. Чотири принципи (SRP, OCP, ISP, DIP) формують <<піраміду модульності>>: SRP і ISP визначають межі, OCP і DIP — способи розширення та зв'язування.
Компонентна архітектура у мобільних проектах (модулі, фічі, шари) виграє від ISP на рівні публічних API. Кожен модуль експортує вузькі інтерфейси для своїх споживачів, а не один загальний фасад. Це дозволяє змінювати внутрішню реалізацію модуля без впливу на споживачів, які використовують лише частину його функціональності.
У Android-проектах з архітектурою Clean Architecture ISP застосовується до UseCase: кожен UseCase — окремий інтерфейс з одним методом invoke або execute. Клієнт (ViewModel) залежить лише від того UseCase, який йому потрібен, а не від цілого репозиторію. Це робить залежності прозорими та тестованими.
Часто задавані питання
Так, надмірне дроблення можливе. ISP не вимагає одного інтерфейсу на кожен метод. Критерій: чи є клієнт, якому потрібна лише частина методів інтерфейсу. Якщо всі клієнти використовують всі методи — інтерфейс не потрібно ділити. Оптимальний рівень дроблення визначається реальними сценаріями використання.
ISP на рівні параметрів означає: функція не повинна приймати об'єкти з великою кількістю полів, якщо використовує лише частину з них. Замість цього варто передавати лише необхідні дані або використовувати спеціалізовані інтерфейси (наприклад, інтерфейс Renderable замість повного User).
LSP про коректне успадкування та поведінкову сумісність підтипів. ISP про проектування інтерфейсів: клієнти не повинні залежати від методів, які не використовують. LSP відповідає на питання «чи можна використовувати підклас замість базового класу?», ISP — «чи потрібен клієнту весь інтерфейс цілком?».
Вузькі інтерфейси спрощують створення mock-об'єктів: тест створює mock з одним-двома методами, а не з десятком. Чим менше методів в інтерфейсі, тим простіше заглушити його поведінку. Це знижує когнітивне навантаження на розробника тесту та зменшує ймовірність помилок у mock-логіці.
Якщо інтерфейс стабільний і всі клієнти використовують всі методи — дроблення надлишкове. Типовий приклад: протоколи UIKit, які проектувала Apple. Розділяти їх ризиковано, оскільки UIKit очікує повної реалізації делегата. У таких випадках порушення ISP виправдане стабільністю API.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також