ISP (Interface Segregation Principle) — den fjärde SOLID-principen som säger: klienter ska inte vara beroende av metoder som de inte använder. Principen formulerades av Robert Martin i samband med design av gränssnitt för objektorienterade system. Som beskrivs i boken Clean Architecture (2017), principen om gränssnittssegregering kräver skapande av snävt specialiserade gränssnitt istället för ett universellt, vilket minskar koppling och förenklar genomförande av ändringar.
Huvudpunkter
ISP (Interface Segregation Principle) — principen om gränssnittssegregering som förbjuder skapandet av “feta” gränssnitt med metoder som inte används av alla klienter. Istället för ett gränssnitt med tio metoder designas flera små gränssnitt, vart och ett för sin egen grupp av klienter.
Principen introducerades av Robert Martin som en lösning på problemet med “gränssnittsförorening”, när en klass tvingas implementera metoder som den inte behöver bara för att de deklareras i ett gemensamt gränssnitt. I statiskt typade språk leder detta till tomma implementationer eller att undantag kastas — ett direkt tecken på ISP-överträdelse.
ISP och SRP kompletterar varandra: SRP handlar om klassens ansvar, ISP handlar om gränssnittens kontrakt. SRP säger ”en klass — en anledning till förändring”, ISP säger ”et gränssnitt — ett klientscenario”. Tillsammans bildar de en modulär arkitektur där varje element i systemet har tydliga gränser.
Fat Interface — ett gränssnitt som innehåller fler metoder än en specifik klient behöver. Till exempel Worker-gränssnittet med metoderna work, eat, sleep. En robotarbetare borde inte implementera eat och sleep, men tvingas. Lösning — uppdelning i Workable, Eatable, Sleepable. Varje klient får precis vad den behöver.
Inom mobilutveckling förekommer feta gränssnitt i delegat- och DataSource-protokoll. Ett protokoll kan innehålla metoder för två olika scenarier (redigering + visning), även om en specifik skärm bara använder ett av dem.
Implementering av ISP börjar med analys av klienterna för varje gränssnitt. Om två klienter använder olika uppsättningar metoder i samma gränssnitt — bör gränssnittet delas upp. Varje nytt gränssnitt grupperar metoder som anropas tillsammans inom ett scenario.
Mekanism för uppdelning: det ursprungliga gränssnittet delas upp i flera smala gränssnitt, som var och en ärver den gemensamma delen (om den finns). Klienter byter till beroende av det nödvändiga smala gränssnittet istället för det allmänna. Klasser som implementerade det ursprungliga gränssnittet implementerar nu bara de smala gränssnitt som de verkligen behöver.
Viktigt förtydligande: graden av uppdelning bestäms av antalet klienter och deras scenarier. ISP kräver inte maximal uppdelning (mikro-gränssnitt med en metod). Detta skulle leda till överdriven komplexitet. Målet är att eliminera klienters beroende av onödiga metoder, inte att minimera storleken på varje gränssnitt.
Huvudsakliga tecken på ISP-överträdelse inkluderar: klasser som implementerar ett gränssnitt med tomma metoder (fiktiv implementation), kastande av UnsupportedOperationException i implementationer, ett stort antal parametrar eller returtyper som inte används av en del av klienterna, och frekventa gränssnittsändringar som bara påverkar en del av klienterna.
I Android-utveckling är ett typiskt exempel på ISP-överträdelse gränssnittet OnItemClickListener som innehåller metoder för klick, långt klick och svep. Om en specifik skärm bara använder klick — förblir de återstående metoderna tomma. Lösning — uppdelning i OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener.
I iOS-utveckling manifesteras ISP-överträdelse i UIKit-delegater: ett protokoll innehåller metoder för olika tillstånd hos en komponent. UITableViewDelegate innehåller metoder för visning, markering, redigering och svep-åtgärder. Ofta implementerar utvecklare hela protokollet med tio tomma metoder. Uppdelning i flera protokoll efter ansvarsgrupper löser problemet.
Problemet är inte bara i kodens estetik. När ett gränssnitt ändras (en ny metod läggs till) måste alla implementerande klasser uppdateras — även de som inte behöver den nya metoden. I mobilutveckling med dussintals skärmar leder detta till kaskadförändringar. ISP isolerar varje klient från förändringar som inte berör dem.
Implicit ISP-överträdelse sker via konfigurationsparametrar. Om en metod tar emot ett objekt med ett stort antal fält, medan klienten bara använder 2–3 av dem — är detta en signal för uppdelning. Alternativ: flera specialiserade metoder med en minimal uppsättning parametrar.
I Android-utveckling överträds ISP när en enda SharedPreferencesManager används för att läsa och skriva alla appinställningar. En Fragment som bara behöver läsa temat får ett beroende till den globala hanteraren med dussintals metoder för olika datatyper. Uppdelning i ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider — tillämpning av ISP på nivån av konfigurationstjänster. Varje leverantör innehåller precis de metoder som dess klienter behöver.
Låt oss titta på Android-exemplet med ett gränssnitt för datahantering. ISP-överträdelse — ett gränssnitt för alla CRUD-operationer, även om inte alla klienter behöver alla operationer.
// ISP-överträdelse: fet gränssnitt
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// Efter tillämpning av ISP: smala gränssnitt
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// ReadOnlyViewModel är inte beroende av skrivmetoder
class ReadOnlyViewModel(
private val reader: UserReader
)
iOS-exempel med uppdelning av protokoll för mediahantering:
// ISP-överträdelse: ett protokoll för all mediahantering
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// Efter ISP: uppdelning i protokoll efter ansvar
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 är inte beroende av laddningsmetoder
class PlayerViewModel {
private let player: MediaPlayer
}
Praktisk slutsats: ISP skyddar klienter från förändringar i oberoende delar av gränssnittet. Uppdelning av UserRepository i UserReader och UserWriter innebär att ändringar i save inte påverkar ReadOnlyViewModel och vice versa. Varje klient är isolerad från funktionalitet som den inte använder och kräver inga ändringar när andra delar av systemet förbättras.
ISP och SRP — ett naturligt par. SRP anger att en klass ska ha en anledning till förändring. ISP tillämpar samma logik på gränssnitt: ett gränssnitt ska tjäna ett klientscenario. En klass kan implementera flera smala gränssnitt (varje motsvarar ett ansvar), vilket är renare än ett fet gränssnitt med flera ansvar.
ISP och OCP är också relaterade: smala gränssnitt är lättare att utöka. Att lägga till en ny metod i ett smalt gränssnitt påverkar bara dess klienter. Att lägga till en metod i ett fet gränssnitt påverkar alla klienter — potentiellt brytande mot OCP, om klienter tvingas ändra sin implementation.
ISP och DIP fungerar tillsammans: DIP kräver beroende av abstraktioner. ISP gör dessa abstraktioner smala och fokuserade. Beroende av ett brett gränssnitt är fortfarande ett beroende av abstraktion, men en “dålig” abstraktion ur ISP-synvinkel. Fyra principer (SRP, OCP, ISP, DIP) bildar “modularitetspyramiden”: SRP och ISP bestämmer gränser, OCP och DIP — sätt att utöka och koppla.
Komponentarkitektur i mobilprojekt (moduler, funktioner, lager) drar nytta av ISP på nivån av publika API:er. Varje modul exporterar smala gränssnitt för sina konsumenter, inte en gemensam fasad. Detta gör det möjligt att ändra modulens interna implementation utan att påverka konsumenter som bara använder en del av dess funktionalitet.
I Android-projekt med Clean Architecture tillämpas ISP på UseCase: varje UseCase är ett separat gränssnitt med en metod invoke eller execute. Klienten (ViewModel) är bara beroende av den UseCase som den behöver, inte hela datalagret. Detta gör beroenden transparenta och testbara.
Vanliga frågor
Ja, överdriven uppdelning är möjlig. ISP kräver inte ett gränssnitt per metod. Kriterium: finns det en klient som bara behöver en del av gränssnittets metoder? Om alla klienter använder alla metoder — behöver gränssnittet inte delas upp. Optimal uppdelningsnivå bestäms av verkliga användningsscenarier.
ISP på parameternivå innebär: en funktion ska inte ta emot objekt med ett stort antal fält om den bara använder en del av dem. Istället bör endast nödvändig data skickas eller specialiserade gränssnitt användas (till exempel Renderable-gränssnittet istället för fullständig User).
LSP handlar om korrekt arv och beteendekompatibilitet hos subtyper. ISP handlar om design av gränssnitt: klienter ska inte vara beroende av metoder som de inte använder. LSP svarar på frågan “kan en underklass användas istället för basklassen?”, ISP — “hehöver klienten hela gränssnittet?”.
Smala gränssnitt förenklar skapandet av mock-objekt: testet skapar en mock med en eller två metoder, inte med tio. Ju färre metoder i ett gränssnitt, desto lättare är det att simulera dess beteende. Detta minskar den kognitiva belastningen på testutvecklaren och minskar sannolikheten för fel i mock-logiken.
Om gränssnittet är stabilt och alla klienter använder alla metoder — är uppdelning överflödig. Typiskt exempel: UIKit-protokoll designade av Apple. Att dela upp dem är riskabelt eftersom UIKit förväntar sig fullständig implementering av delegaten. I sådana fall är ISP-överträdelse motiverad av API:ets stabilitet.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också