Table of Contents
In objectgerichte programmering, bouwsystemen die zowel robuust als aanpasbaar zijn, is de permanente uitdaging. Onder de fundamentele principes die ontwikkelaars richting dat doel leiden is het Liskov Substitutie Principe (LSP). Ontworpen door Barbara Liskov in 1987, LSP is de derde pijler van de vijf SOLID principes voor software-ontwerp, en het richt zich op een kritische vraag: wanneer u een subklasse die erft van een ouderklasse, kunt u veilig elk geval van de ouder vervangen door een instantie van het kind zonder het programma te breken? Het principe is een definitieve . . . . . . maar alleen als de subklasse respecteert het contract dat door de ouder is gedefinieerd. Dit artikel onderzoekt de Liskov Substitutie Principe in detail, het verstrekken van duidelijke definities, praktische voorbeelden, en strategieën om ervoor te zorgen dat uw klasse hiërarchieën betrouwbaar en flexibel blijven.
Wat is het Liskov Substitutie Principe?
Het Liskov Substitutie Principe stelt dat objecten van een superklasse vervangbaar moeten zijn met objecten van zijn subklassen zonder de juistheid van het programma te beïnvloeden. Met andere woorden, als een functie of methode is ontworpen om te werken met een basistype, moet het ook werken met een afgeleid type zonder dat wijziging of het produceren van onverwachte bijwerkingen vereist. Barbara Liskov eerst verwoordde dit idee in haar 1987 keynote adres op de Conferentie over Object-georiënteerde Programmering Systemen, Talen en Toepassingen (OOPPLA), en het is sindsdien een hoeksteen van object-georiënteerd ontwerp geworden.
LSP is fundamenteel over gedrag subtyping. Het is niet genoeg dat een subklasse dezelfde methode handtekeningen als zijn ouder heeft (syntactische conformantie); de subklasse moet ook de intenties en beperkingen van de ouderklasse respecteren. Als een subklasse verandert het fundamentele gedrag van een ouder methode . . bijvoorbeeld, door het gooien van een uitzondering de ouder nooit gooit, het teruggeven van een waarde die de ouder contract schendt, of het vereisen van strengere voorwaarden . Dan is de vervanging niet veilig, en het ontwerp schendt LSP.
Formele definitie en achtergrond
Barbara Liskov... de oorspronkelijke formele definitie is als volgt:
Deze definitie benadrukt dat een subtype (S) moet worden vervangen door zijn supertype (T) in elk programma (P) dat wordt geschreven in termen van T. Het programma . Onwaarneembare gedrag moet worden bewaard. Dit concept is nauw gerelateerd aan de Design by Contract (DbC) methodologie, pioniers van Bertrand Meyer, waar elke methode expliciete voorwaarden heeft (wat moet waar zijn voor de methode loopt) en postvoorwaarden (wat moet waar zijn na). Onder LSP, subklassen kunnen alleen verzwakken voorwaarden en versterken postvoorwaarden; ze kunnen het tegenovergestelde niet doen zonder te breken .
Zie voor meer informatie het originele Liskov en vleugelpapier dat het concept formaliseerde. Daarnaast geeft het Wikipedia artikel over LSP een goed overzicht.
Waarom is LSP belangrijk?
Het aanhangen van LSP brengt verschillende kritische voordelen met zich mee voor objectgeoriënteerde systemen:
- Betrouwbaarheid en correctheid: Code die een basistype gebruikt kan erop vertrouwen dat elke subklasse zich zal gedragen volgens het basistype. Dit voorkomt subtiele bugs die optreden wanneer een subklasse onverwacht gedrag introduceert.
- Polymorfisme: Polymorfisme is het vermogen om objecten van verschillende klassen te behandelen via een gemeenschappelijke interface. Zonder LSP wordt polymorfisme gevaarlijk omdat vervanging van een subklasse onjuiste resultaten kan opleveren. LSP zorgt ervoor dat polymorfe code werkt zoals bedoeld.
- Behoud en duurzaamheid: Wanneer LSP-schendingen worden vermeden, is het toevoegen van nieuwe subklassen niet nodig bestaande code te wijzigen die afhankelijk is van het basistype. Dit sluit aan bij het Open/Closed Principle (OCP) . software-entiteiten moeten open zijn voor uitbreiding maar gesloten voor wijziging.
- Testabiliteit: Eenheidstests die tegen een basisklasse zijn geschreven, kunnen worden hergebruikt om subklassen te valideren. Als een subklasse LSP schendt, zullen deze tests mislukken, waardoor de inconsistentie vroeg wordt onthuld.
LSP is niet alleen een academisch concept; het heeft directe praktische implicaties. Bijvoorbeeld, in een betalingsverwerkingssysteem, als je een basis klasse met een methode hebt, verwacht je dat alle subklassen (bijv. , ) betalingen zonder fouten of bijwerkingen verwerken die de basisklasse niet verwacht. Schendingen kunnen hier leiden tot verlies van inkomsten of beschadigde gegevens.
Gemeenschappelijke inbreuken op het Substitutiebeginsel van Liskov
Herkennen LSP overtredingen is de eerste stap naar het bevestigen van hen. Hier zijn een aantal typische patronen die het principe breken:
Versterking van de voorwaarden
Als een basisklasse methode een integer parameter verwacht, zal het toevoegen van een voorwaarde in de subklasse dat het gehele getal positief moet zijn (terwijl de basisklasse een geheel getal accepteert) de voorwaarde versterken. Klanten die een negatief getal aan de basisklasse hebben doorgegeven, zouden nu falen met de subklasse. Voorbeeld:
// Base class
class UserService {
public void assignRole(int userId) { ... }
}
// Subclass violation
class AdminService extends UserService {
@Override
public void assignRole(int userId) {
if (userId <= 0) throw new IllegalArgumentException("Invalid ID");
...
}
}
Verzwakke postvoorwaarden
Als de basisklasse een bepaalde rendementswaarde of neveneffect garandeert, dan is een subklasse die de garantie vermindert in strijd met LSP. Zo kan een basisklassemethode altijd een niet-null string teruggeven; een subklasse die teruggeeft, verzwakt in sommige gevallen de postconditie.
Nieuwe uitzonderingen gooien
Subklassen mogen geen uitzonderingen maken die de basisklasse niet gooit (tenzij die uitzonderingen subklassen van uitzonderingen zijn die reeds zijn toegestaan). Als klanten van de basisklasse alleen vangen , en een subklasse gooit een , zal de vervanging onverwachte crashs veroorzaken.
Verwijderen of overrijden van methoden die moeten worden geërfd
Als een subklasse een methode overschrijft om niets te doen (leeg lichaam) of een te gooien, dan is dat een duidelijke overtreding. De subklasse gedraagt zich niet zoals de basisklasse bedoeld.
Klassiek voorbeeld: Rechthoek, Vierkant, en de Area Trap
Het meest geciteerde voorbeeld van LSP overtreding gaat geometrische vormen. Veel leerboeken beginnen met een klasse die en methoden heeft, en dan een subklasse aanmaken die deze overschrijft om breedte == hoogte te handhaven. Hier is het probleem:
// Base class
class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
// Subclass violation
class Square extends Rectangle {
@Override
public void setWidth(int w) {
width = height = w;
}
@Override
public void setHeight(int h) {
width = height = h;
}
}
Beschouw nu een functie die werkt met een :
void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
assert r.getArea() == 50; // Expect 50
}
Wanneer een ] voorbij is, stelt de methode de breedte in op 5 en vervolgens de hoogte op 10 .De vierkanten staan overbelast zet ook de breedte op 10, zodat het vierkant eindigt met breedte=10, hoogte=10, oppervlakte=100. De bewering mislukt. De is niet substitueerbaar voor omdat het de postconditie verandert: na en , is een rechthoekoppervlak 50, maar een vierkantsoppervlak is 100.
De fix: Vermijd erven ] van . Ontwerp in plaats daarvan een abstracte klasse met een gemeenschappelijke ] methode, en laat beide [ en uitvoeren onafhankelijk. Gebruik ook compositie: een vierkant kan een rechthoek zijn met gelijke zijden, maar het blootleggen van sets die de invariant breken is het echte probleem. Het principe wordt vaak samengevat als Dont breken van het contract.
Voorbeeld Real-World: Integratie van betalingspoorten
Stel je een e-commercesysteem voor met een basisklasse :
abstract class PaymentGateway {
public abstract void charge(double amount);
public void refund(double amount) { /* default implementation */ }
}
Subklassen omvatten en . Een cliënt die betalingen verwerkt kan en later oproepen. Een overtreding treedt op als ] ] een uitzondering gooit omdat PayPal een terugbetalings-ID vereist, niet alleen een bedrag. Nu zal elke code die gebruikt op een ]-referentie breken wanneer het runtime-type is.
Hoe te repareren: Ofwel (a) zorgt ervoor dat de basisklasseovereenkomst de mogelijkheid omvat dat niet ondersteund wordt (bijvoorbeeld, het terugsturen van een succesbooleaans of een gecontroleerde uitzondering aangeven), of (b) de hiërarchie herontworpen zodat niet alle gateways restitutie ondersteunen. Bijvoorbeeld, een interface introduceert en alleen gateways die restituties ondersteunen, het implementeren. De klant controleert dan voordat hij terugbetaling aanroept. Dit respecteert LSP omdat de basis geen werkteruggaafmethode belooft.
Hoe het Liskov Substitutiebeginsel in de praktijk te volgen
De implementatie van LSP vereist discipline in ontwerp en testen. Hier zijn de te gebruiken richtlijnen:
- Ontwerp per contract: Geef duidelijk voorwaarden, postvoorwaarden en invarianten voor basisklassemethoden. Documenteer wat elke methode verwacht en garandeert. Zorg er dan voor dat elke subklasse voldoet. Tools zoals Contracten in .NET of JML voor Java kunnen helpen om deze regels te handhaven.
- Favoriete Interfaces over Abstract Classes: Interfaces definiëren een contract zonder implementatiedetails. Ze zijn natuurlijk afgestemd op LSP omdat elke implementatieklasse de volledige interface moet vervullen. Met abstracte klassen is het gemakkelijker om per ongeluk afhankelijkheden in te voeren.
- Gebruik Compositie Over Erfelijkheid: Wanneer een subklasse gedrag zou moeten overschrijven tot het punt van het breken van het basiscontract, is het vaak beter om compositie te gebruiken. Bijvoorbeeld, in plaats van erven van , hebben een klasse die intern gebruik maakt van een maar de setters niet onthult.
- Test voor substitueerbaarheid: Schrijf geparametriseerde tests die dezelfde scenario's uitvoeren tegen zowel basis- als afgeleide types. Als een test slaagt met het basistype maar niet met een afgeleid type, heb je een LSP overtreding. Deze praktijk is vooral krachtig bij het gebruik van testdubbelen.
- Controleer op Covator Return Types: Sommige talen laten coovator retour typen (bijvoorbeeld, een subklasse methode kan een specifiekere type dan de basis retourneren). Dit is prima zolang het niet verandert de postconditie. Zorg ervoor dat het geretourneerde object nog steeds voldoet aan alle verwachtingen van het basistype .
- Vermijd Overriding Concrete Methoden Onopvallend: Als je de noodzaak voelt om een concrete methode in een subklasse te omzeilen, vraag je dan of erfdeel het juiste hulpmiddel is. Misschien was de basisklasse te concreet. Maak de methode abstract of virtueel alleen wanneer je van plan bent subklassen om gedrag op een gecontroleerde manier te veranderen.
LSP en de andere SOLID-beginselen
LSP is nauw verbonden met de andere SOLID-beginselen, met name het Open/Gesloten Principe (OCP) en het Inversiebeginsel voor afhankelijkheid (DIP):
- LSP en OCP: OCP stelt dat klassen open moeten zijn voor uitbreiding maar gesloten voor wijziging. LSP zorgt ervoor dat extensies (subklassen) de bestaande code die het basistype gebruikt niet breken. Zonder LSP, zou het toevoegen van een nieuwe subklasse vereisen dat de clientcode wordt aangepast om het nieuwe gedrag te behandelen, waardoor OCP wordt geschonden.
- LSP en DIP: DIP adviseert afhankelijk van abstracties, niet concreties. LSP is hier essentieel omdat de abstractie (interface of basisklasse) stabiel en betrouwbaar moet zijn. Als subklassen LSP schenden, is de abstractie niet langer een betrouwbare afhankelijkheid, en wordt het systeem kwetsbaar.
- LSP en Interface Segregation (ISP): ISP stimuleert kleine, gerichte interfaces. Dit ondersteunt natuurlijk LSP omdat een kleine interface een strak contract definieert dat gemakkelijker te eren is. Een klasse die een oversized interface implementeert kan moeite hebben om alle onderdelen te vervullen, wat leidt tot schendingen zoals gooien .
Voor een uitgebreid overzicht van de SOLID-beginselen, kijk op Wikipedia
Conclusie
Het Liskov Substitutie Principe is veel meer dan een theoretische fraaiheid; het is een praktisch hulpmiddel voor het bouwen van objectgerichte systemen die veilig uit te breiden en gemakkelijk te onderhouden zijn. Door ervoor te zorgen dat subklassen zich gedragen op een manier die vervangbaar is voor hun ouderklassen, kunnen ontwikkelaars vertrouwen op polymorfisme zonder angst voor verborgen bugs. Het principe moedigt zorgvuldige gedachte over klassehiërarchieën, wat leidt tot ontwerpen die compositie boven erfenis, duidelijk gedefinieerde contracten, en robuuste testen.
Onthoud, LSP overtredingen verschijnen vaak als subtiele inconsistenties in gedrag: een methode die een onverwachte uitzondering werpt, een methode die stil niets doet, of een methode die de staat verandert op een manier die de basisklasse nooit bedoeld heeft. Door deze rode vlaggen in acht te nemen, en door de hierboven besproken richtlijnen toe te passen, kun je hiërarchieën creëren die de tand des tijds doorstaan. Barbara Liskov.Inzicht blijft ontwikkelaars leiden naar meer betrouwbare en flexibele software. Voor verder onderzoek, overweeg het lezen van Robert C. Martins Agile Software Development: Principes, Patterns, and Practices[, die uitgebreide voorbeelden van LSP en de andere SOLID principes biedt.
Externe verwijzingen die in dit artikel worden gebruikt:
- A Gedragsaanduiding van subtyping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
- Wikipedia: Liskov Substitutiebeginsel
- Wikipedia: SOLID Principles
- Wikipedia: Ontwerp per contract