LSP: essentie, het substitutieprincipe van Barbara Liskov in ontwikkeling

Auteur: IT Sectr Gepubliceerd: 2026-05-12 Leestijd: 9 min

LSP (Liskov Substitution Principle) — het derde SOLID-principe dat de voorwaarden voor correcte overerving in objectgeoriënteerd programmeren bepaalt. Het principe werd in 1987 geformuleerd door Barbara Liskov en als volgt geformaliseerd: als S een subtype is van T, dan kunnen objecten van T worden vervangen door objecten van S zonder de eigenschappen van het programma te wijzigen. Zoals opgemerkt in het boek van Robert Martin Clean Architecture (2017), vereist het substitutieprincipe dat een subklasse het contract van de basisklasse niet verzwakt.

Belangrijkste punten

  • LSP — het Liskov-substitutieprincipe, het derde SOLID-principe over correcte overerving
  • Subklasse moet het contract van de basisklasse behouden — precondities en postcondities
  • Schending van LSP uit zich in het vierkant-rechthoek probleem en gegoocide uitzonderingen
  • Compositie heeft vaak de voorkeur boven overerving voor het naleven van LSP
  • Contractgericht ontwerpen (Design by Contract) — formele manier om LSP te controleren

Wat is LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) — het substitutieprincipe geformuleerd door Barbara Liskov op de OOPSLA-conferentie in 1987. Formele definitie: laat q(x) een aantoonbare eigenschap zijn van objecten x van type T. Dan moet q(y) aantoonbaar zijn voor objecten y van type S, waarbij S een subtype is van T. Simpel gezegd: objecten van een subklasse moeten zich zo gedragen dat code die met de basisklasse werkt, correct blijft werken met de subklasse.

In de praktijk betekent LSP dat een subklasse het contract van de basisklasse niet mag schenden. Het contract omvat precondities (wat nodig is om een methode aan te roepen), postcondities (wat gegarandeerd wordt na aanroep) en invarianten (voorwaarden die gedurende de levensduur van het object behouden blijven). Een subklasse kan precondities versterken of postcondities verzwakken — dat is een schending van LSP.

Het klassieke voorbeeld van LSP-schending — een vierkant dat overerft van een rechthoek. De methode setWidth bij een rechthoek stelt de breedte in, bij een vierkant — zowel de breedte als de hoogte. Een client die het gedrag van een rechthoek verwacht (wijziging van één zijde beïnvloedt de andere niet) krijgt een onverwacht resultaat. Een vierkant is geen correct subtype van een rechthoek.

Formele voorwaarden van LSP

LSP stelt drie voorwaarden voor correcte overerving: precondities van de subklasse mogen niet sterker zijn dan precondities van de basisklasse (subklasse vereist niet meer), postcondities van de subklasse mogen niet zwakker zijn dan postcondities van de basisklasse (subklasse garandeert niet minder), invarianten van de basisklasse moeten behouden blijven in de subklasse. Deze voorwaarden staan bekend als de regel van contractgericht ontwerpen van Bertrand Meyer.

Als ten minste één voorwaarde wordt geschonden — kan code die polymorfisme gebruikt, defect raken. De compiler controleert alleen syntactische, niet semantische contracten. Daarom is LSP een kwestie van architectuurdiscipline, niet van statische typering.

Hoe werkt het Liskov-substitutieprincipe

Het LSP-mechanisme is gebaseerd op gedragscompatibiliteit van typen. Als klasse S overerft van klasse T, moet clientcode S overal kunnen gebruiken waar T wordt verwacht, zonder gedragsverandering. Dit omvat niet alleen methodehandtekeningen, maar ook hun semantiek.

LSP verbiedt een subklasse niet om nieuw gedrag toe te voegen. Het schenden van verwachtingen van code geschreven voor de basisklasse is verboden. Als de basisklasse garandeert dat de methode save geen uitzonderingen gooit, mag de subklasse ze niet gooien. Als de basisklasse een niet-negatieve waarde retourneert, mag de subklasse geen negatieve retourneren.

In echte projecten wordt LSP het vaakst geschonden door het toevoegen van conditionele logica in methoden van de subklasse: „als voorwaarde — gooi uitzondering”, „als voorwaarde — retourneer null”. Elke dergelijke „verrassing” ondermijnt polymorfisme en dwingt clientcode om het type object te controleren vóór aanroep — wat in tegenspraak is met het idee van objectgeoriënteerd ontwerpen.

In mobiele projecten komt typische LSP-schending voor bij het maken van basis ViewModels. Als BaseViewModel garandeert dat de methode onCleared alle bronnen vrijgeeft, en de subklasse overschrijft deze methode als leeg — dan zal alle code die vertrouwt op het vrijgeven van bronnen via polymorfe aanroep van onCleared, incorrect werken. LSP vereist dat de subklasse ofwel super.onCleared() aanroept, ofwel zelf hetzelfde werk uitvoert. Compositie via LifecycleObserver — een alternatief dat LSP-schending bij levenscyclusbeheer uitsluit.

Tekenen van LSP-schending in code

Belangrijkste indicatoren van LSP-schending zijn onder meer: controle van het objecttype via instanceof of is vóór methodeaanroep, lege methode-implementaties (stubs), het gooien van NotImplementedError of UnsupportedOperationException, het retourneren van null in plaats van een waarde. Elk van deze patronen geeft aan dat de subklasse geen correct subtype is.

Een ander veelvoorkomend teken — overerving met het doel code te hergebruiken, niet om de „is-een”-relatie (is-a) te modelleren. Klasse Bird heeft een methode fly(). Klasse Penguin erft van Bird en overschrijft fly() als leeg of een uitzondering gooiend. Dit is LSP-schending: een pinguïn is geen correct subtype van een vogel.

In mobiele ontwikkeling wordt LSP geschonden bij het maken van basis ViewHolder, Fragment of ViewController met stub-methoden. Als een subklasse de helft van de methoden van de basisklasse niet gebruikt — is overerving verkeerd gekozen. Compositie of interface-segregatie lossen het probleem correcter op.

LSP-test

Eenvoudige test voor LSP-controle: schrijf een unittest voor de basisklasse die het contract controleert (retourwaarden, uitzonderingen, bijwerkingen). Voer deze test uit voor elke subklasse. Als de test faalt — is LSP geschonden. Deze benadering heet „testen via het contract van de basisklasse”.

In Android-projecten is zo'n test nuttig voor ViewModel en Repository. Als BaseViewModel de status Loading vóór een fout garandeert, en de subklasse gooit een fout zonder Loading — zal de test de LSP-schending in de CI-fase vastleggen.

LSP-voorbeelden in mobiele ontwikkeling

Laten we het Android-voorbeeld met ClickListener-verwerking bekijken. LSP-schending treedt op wanneer de basisimplementatie iets garandeert en de subklasse dit schendt.

kotlin
// Basisklasse met garantie: onClick wordt aangeroepen
open class BaseClickListener {
    open fun onClick(view: View) {
        // basisverwerking
    }
}

// LSP-schending: subklasse voegt een voorwaarde toe die een uitzondering gooit
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Correcte oplossing: contract niet geschonden
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Het iOS-voorbeeld met het DataSource-protocol toont LSP-schending door nil te retourneren in plaats van gegevens:

swift
// Protocol met contract: retourneert gegevens of fout
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// LSP-schending: retourneert nil zonder fout
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // lege array in plaats van fout
    }
}

// Correcte naleving van LSP
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Praktische regel: als een subklasse het contract van de basisklasse niet kan nakomen — moet het geen subklasse zijn. Alternatief — een interface met een minimaal contract extraheren en deze in elk type op zijn eigen manier implementeren.

LSP en overerving: wanneer compositie te kiezen

Compositie heeft de voorkeur boven overerving in situaties waar de „is-een”-relatie (is-a) dubbelzinnig of voorwaardelijk is. Klassiek voorbeeld: Manager is Employee? Ja. Maar is Square een correcte Rectangle? LSP zegt „nee”. Als u twijfelt over de correctheid van overerving — kies dan compositie.

In mobiele ontwikkeling wordt compositie vaak gebruikt via dependency injection: in plaats van gedrag te erven van een basisklasse, krijgt de klasse het via de constructor. ViewModel erft niet van Repository, maar accepteert het als afhankelijkheid. Dit sluit LSP-schending per definitie uit — geen overerving, geen contractschending.

Tekenen dat overerving moet worden vervangen door compositie: de subklasse gebruikt een deel van de methoden van de basisklasse niet, de subklasse overschrijft methoden met lege stubs, clientcode controleert het objecttype via instanceof. In deze gevallen is overerving verkeerd gekozen en is LSP geschonden.

Oplossing via interfaces

Interfaces lossen het LSP-probleem op zonder overerving: elk type implementeert precies de methoden die het nodig heeft. In plaats van een gemeenschappelijke basisklasse Bird met de methode fly (waar Penguin niet vliegt) — de interface Flyable, die alleen door vliegende vogels wordt geïmplementeerd. Penguin implementeert Bird zonder de methode fly — LSP is niet geschonden.

In Android-architectuur wordt deze benadering toegepast via gesegregeerde interfaces UseCase: in plaats van één grote UseCase met methoden getAll, getById, save, delete — afzonderlijke interfaces GetItemsUseCase, SaveItemUseCase. De client is alleen afhankelijk van de benodigde interface en elke klasse die deze interface implementeert, is correct vanuit LSP-oogpunt.

Veelgestelde vragen

Wat is het verschil tussen LSP en gewone overerving?

Overerving — een taalmechanisme, LSP — een regel voor correct gebruik van dit mechanisme. Overerving garandeert compatibiliteit van handtekeningen (syntaxis), LSP vereist compatibiliteit van gedrag (semantiek). Overerving zonder LSP geeft polymorfisme dat tijdens runtime kapot gaat.

Schendt null in een subklasse altijd LSP?

Als de basisklasse een non-null retour garandeert — ja. Als het contract null toestaat (optionele waarde) — nee. LSP verbiedt null niet, het verbiedt verzwakking van het contract. Bestudeer de documentatie van de basisklasse en controleer of het contract van de subklasse compatibel is.

Hoe wordt LSP toegepast op protocollen in Swift?

Op protocollen wordt LSP op dezelfde manier toegepast als op klassen. De implementatie van een protocol moet het semantische contract naleven: als het protocol een methode als non-throwing definieert, mag de implementatie geen fouten gooien. Swift controleert dit niet op compilatieniveau — de verantwoordelijkheid ligt bij de ontwikkelaar.

Kan LSP worden geschonden bij gebruik van sealed class?

Sealed class in Kotlin is een speciaal geval, omdat de hiërarchie gesloten en bekend is bij de compiler. LSP is in mindere mate van toepassing op sealed class, omdat alle subtypes expliciet worden vermeld in de when-expressie. Een fout van een sealed-subklasse zal lokaal zijn, geen verborgen polymorfe fout.

Hoe test je de naleving van LSP in een project?

Schrijf een geparametriseerde test voor de basisklasse die wordt uitgevoerd voor al zijn subklassen. De test controleert de belangrijkste gedragscontracten: retourwaarden, uitzonderingen, toestanden. Als de test faalt voor een van de subklassen — is LSP geschonden. In CI voorkomt een dergelijke test regressie van polymorfe code.

Samenvatting

  • LSP (Liskov Substitution Principle) — het substitutieprincipe, derde in SOLID, over semantische compatibiliteit van overerving
  • Subklasse moet het contract van de basisklasse behouden: precondities, postcondities en invarianten
  • Instanceof-controle en lege methode-overschrijvingen — belangrijkste tekenen van LSP-schending
  • Compositie en interfaces lossen het LSP-probleem op waar overerving incorrect is
  • Het vierkant-rechthoek probleem — klassiek voorbeeld van subtype-incompatibiliteit
  • Contracttest voor de basisklasse, uitgevoerd voor alle subklassen, detecteert LSP-schending in CI
  • Sealed class in Kotlin vermindert LSP-risico's door gesloten hiërarchie die bekend is bij de compiler

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.

Bespreek het project

Lees ook