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-a. Четири принципа (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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође