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-действия. Често разработчиците имплементират целия протокол с десет празни метода. Разделянето на няколко протокола по групи отговорности решава проблема.
Проблемът не е само в естетиката на кода. Когато интерфейсът се промени (добави се нов метод), всички имплементиращи класове трябва да бъдат актуализирани — дори тези, които не се нуждаят от новия метод. В мобилната разработка с десетки екрани това води до каскадни промени. 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също