ISP (Interface Segregation Principle) — het vierde SOLID-principe dat stelt: clients mogen niet afhankelijk zijn van methoden die ze niet gebruiken. Het principe werd geformuleerd door Robert Martin in de context van het ontwerpen van interfaces voor objectgeoriënteerde systemen. Zoals beschreven in het boek Clean Architecture (2017), het principe van interfacescheiding vereist het creëren van smal gespecialiseerde interfaces in plaats van één universele, wat de koppeling vermindert en het aanbrengen van wijzigingen vereenvoudigt.
Belangrijkste
ISP (Interface Segregation Principle) — het principe van interfacescheiding dat het maken van “dikke” interfaces verbiedt met methoden die niet door alle clients worden gebruikt. In plaats van één interface met tien methoden worden meerdere kleine interfaces ontworpen, elk voor zijn eigen groep clients.
Het principe werd geïntroduceerd door Robert Martin als oplossing voor het probleem van “interfacevervuiling”, wanneer een klasse gedwongen wordt methoden te implementeren die hij niet nodig heeft, alleen omdat ze in een gemeenschappelijke interface zijn gedeclareerd. In statisch getypeerde talen leidt dit tot lege implementaties of het gooien van uitzonderingen — een direct teken van ISP-schending.
ISP en SRP vullen elkaar aan: SRP gaat over de verantwoordelijkheid van een klasse, ISP gaat over de contracten van interfaces. SRP zegt “één klasse — één reden voor verandering”, ISP zegt “één interface — één clientscenario”. Samen vormen ze een modulaire architectuur waarbij elk element van het systeem duidelijke grenzen heeft.
Fat Interface — een interface die meer methoden bevat dan een specifieke client nodig heeft. Bijvoorbeeld de Worker-interface met methoden work, eat, sleep. Een robotarbeider zou eat en sleep niet moeten implementeren, maar wordt gedwongen. Oplossing — opsplitsen in Workable, Eatable, Sleepable. Elke client krijgt precies wat hij nodig heeft.
In mobiele ontwikkeling komen dikke interfaces voor in delegate- en DataSource-protocollen. Eén protocol kan methoden bevatten voor twee verschillende scenario's (bewerken + weergeven), terwijl een specifiek scherm slechts één ervan gebruikt.
Implementatie van ISP begint met analyse van de clients van elke interface. Als twee clients verschillende sets methoden van dezelfde interface gebruiken — moet de interface worden opgesplitst. Elke nieuwe interface groepeert methoden die samen worden aangeroepen binnen één scenario.
Mechanisme van opsplitsing: de oorspronkelijke interface wordt verdeeld in meerdere smalle interfaces, die elk het gemeenschappelijke deel (indien aanwezig) erven. Clients schakelen over naar afhankelijkheid van de benodigde smalle interface in plaats van de algemene. Klassen die de oorspronkelijke interface implementeerden, implementeren nu alleen die smalle interfaces die ze echt nodig hebben.
Belangrijke verduidelijking: de mate van opsplitsing wordt bepaald door het aantal clients en hun scenario's. ISP vereist geen maximale opsplitsing (micro-interfaces met één methode). Dit zou leiden tot overmatige complexiteit. Het doel is het elimineren van afhankelijkheid van clients van onnodige methoden, niet het minimaliseren van de grootte van elke interface.
Belangrijkste tekenen van ISP-schending zijn onder meer: klassen die een interface implementeren met lege methoden (fictieve implementatie), het gooien van UnsupportedOperationException in implementaties, een groot aantal parameters of retourtypen die niet door een deel van de clients worden gebruikt en frequente interfacewijzigingen die slechts een deel van de clients treffen.
In Android-ontwikkeling is een typisch voorbeeld van ISP-schending de OnItemClickListener-interface die methoden bevat voor klikken, lang klikken en vegen. Als een specifiek scherm alleen klikken gebruikt — blijven de overige methoden leeg. Oplossing — opsplitsen in OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener.
In iOS-ontwikkeling manifesteert ISP-schending zich in UIKit-delegates: één protocol bevat methoden voor verschillende toestanden van een component. UITableViewDelegate bevat methoden voor weergave, selectie, bewerking en swipe-acties. Vaak implementeren ontwikkelaars het hele protocol met tien lege methoden. Opsplitsing in meerdere protocollen per verantwoordelijkheidsgroep lost het probleem op.
Het probleem zit niet alleen in de esthetiek van code. Wanneer een interface verandert (een nieuwe methode wordt toegevoegd), moeten alle implementerende klassen worden bijgewerkt — zelfs degenen die de nieuwe methode niet nodig hebben. In mobiele ontwikkeling met tientallen schermen leidt dit tot cascadeveranderingen. ISP isoleert elke client van veranderingen die hem niet aangaan.
Impliciete ISP-schending vindt plaats via configuratieparameters. Als een methode een object met een groot aantal velden ontvangt, terwijl de client slechts 2-3 ervan gebruikt — is dit een signaal voor opsplitsing. Alternatief: meerdere gespecialiseerde methoden met een minimale set parameters.
In Android-ontwikkeling wordt ISP geschonden bij gebruik van één SharedPreferencesManager voor het lezen en schrijven van alle app-instellingen. Een Fragment dat alleen het thema hoeft te lezen, krijgt een afhankelijkheid van de globale manager met tientallen methoden voor verschillende gegevenstypen. Opsplitsen in ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider — toepassing van ISP op het niveau van configuratiediensten. Elke provider bevat precies de methoden die zijn clients nodig hebben.
Laten we het Android-voorbeeld bekijken met een interface voor gegevensverwerking. ISP-schending — één interface voor alle CRUD-bewerkingen, hoewel niet alle clients alle bewerkingen nodig hebben.
// ISP-schending: dikke interface
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// Na toepassing van ISP: smalle interfaces
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// ReadOnlyViewModel is niet afhankelijk van schrijfmethode
class ReadOnlyViewModel(
private val reader: UserReader
)
iOS-voorbeeld met opsplitsing van protocollen voor mediabeheer:
// ISP-schending: één protocol voor al het mediabeheer
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// Na ISP: opsplitsing in protocollen per verantwoordelijkheid
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 is niet afhankelijk van laadmethoden
class PlayerViewModel {
private let player: MediaPlayer
}
Praktische conclusie: ISP beschermt clients tegen veranderingen in onafhankelijke delen van de interface. Opsplitsing van UserRepository in UserReader en UserWriter betekent dat wijzigingen in save geen invloed hebben op ReadOnlyViewModel en vice versa. Elke client is geïsoleerd van functionaliteit die hij niet gebruikt en vereist geen wijzigingen bij verbetering van andere delen van het systeem.
ISP en SRP — een natuurlijk paar. SRP bepaalt dat een klasse één reden voor verandering moet hebben. ISP past dezelfde logica toe op interfaces: een interface moet één clientscenario bedienen. Een klasse kan meerdere smalle interfaces implementeren (elke komt overeen met één verantwoordelijkheid), wat schoner is dan één dikke interface met meerdere verantwoordelijkheden.
ISP en OCP zijn ook gerelateerd: smalle interfaces zijn gemakkelijker uit te breiden. Toevoeging van een nieuwe methode aan een smalle interface treft alleen zijn clients. Toevoeging van een methode aan een dikke interface treft alle clients — mogelijk OCP schendend, als clients gedwongen worden hun implementatie te wijzigen.
ISP en DIP werken samen: DIP vereist afhankelijkheid van abstracties. ISP maakt deze abstracties smal en gefocust. Afhankelijkheid van een brede interface is nog steeds een afhankelijkheid van abstractie, maar een “slechte” abstractie vanuit ISP-oogpunt. Vier principes (SRP, OCP, ISP, DIP) vormen de “piramide van modulariteit”: SRP en ISP bepalen grenzen, OCP en DIP — manieren van uitbreiding en koppeling.
Componentarchitectuur in mobiele projecten (modules, features, lagen) profiteert van ISP op het niveau van publieke API's. Elke module exporteert smalle interfaces voor zijn consumenten, niet één gemeenschappelijke façade. Dit maakt het mogelijk de interne implementatie van de module te wijzigen zonder invloed op consumenten die slechts een deel van de functionaliteit gebruiken.
In Android-projecten met Clean Architecture wordt ISP toegepast op UseCase: elke UseCase is een aparte interface met één methode invoke of execute. De client (ViewModel) hangt alleen af van de UseCase die hij nodig heeft, niet van de hele repository. Dit maakt afhankelijkheden transparant en testbaar.
Veelgestelde vragen
Ja, overmatige opsplitsing is mogelijk. ISP vereist niet één interface per methode. Criterium: is er een client die slechts een deel van de interfacemethoden nodig heeft? Als alle clients alle methoden gebruiken — hoeft de interface niet te worden opgesplitst. Het optimale opsplitsingsniveau wordt bepaald door reële gebruiksscenario's.
ISP op parameterniveau betekent: een functie mag geen objecten met een groot aantal velden accepteren als ze slechts een deel ervan gebruikt. In plaats daarvan moeten alleen de benodigde gegevens worden doorgegeven of gespecialiseerde interfaces worden gebruikt (bijvoorbeeld de Renderable-interface in plaats van volledige User).
LSP gaat over correcte overerving en gedragscompatibiliteit van subtypen. ISP gaat over het ontwerpen van interfaces: clients mogen niet afhankelijk zijn van methoden die ze niet gebruiken. LSP beantwoordt de vraag “kan een subklasse worden gebruikt in plaats van de basisklasse?”, ISP — “heeft de client de hele interface nodig?”.
Smalle interfaces vereenvoudigen het maken van mock-objecten: de test maakt een mock met één of twee methoden, niet met tien. Hoe minder methoden in een interface, hoe gemakkelijker het is om het gedrag te simuleren. Dit vermindert de cognitieve belasting van de testontwikkelaar en verkleint de kans op fouten in de mock-logica.
Als de interface stabiel is en alle clients alle methoden gebruiken — is opsplitsing overbodig. Typisch voorbeeld: UIKit-protocollen ontworpen door Apple. Ze opsplitsen is riskant omdat UIKit een volledige implementatie van de delegate verwacht. In dergelijke gevallen is ISP-schending gerechtvaardigd door de stabiliteit van de API.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook