Definition des LSP
"Überall, wo der Typ einer Superklasse zulässig ist, ist auch der Typ einer Subklasse erlaubt". [1]
Beispiel
Dieses wichtige Prinzip soll hier an einem Beispiel aus dem Bibliotheks-Projekt (Folge 7.4 des OOP-Kurses) erläutert werden.
public class LSP_Demo
{
Buch roman = new Roman("Der Herr der Ringe",
"J.R. Tolkien",
"Fantasy",
1964);
public LSP_Demo()
{
roman.zeige();
}
}
Anwendung des LSP auf dieses Beispiel
1. Erwartungen des Programms
Die Referenzvariable
Buch roman ...
signalisiert dem Programm:
- roman ist ein Objekt der Klasse Buch
- roman besitzt alle Instanzvariablen und Methoden, die in Buch definiert sind
- roman besitzt eine Methode zeige()(weil es ein Buch-Objekt ist)
2. Substitution durch eine Unterklasse
roman = new Roman(...);
Der Buch-Variablen roman wird ein Roman-Objekt zugewiesen. Nach dem LSP ist zulässig.
3. Methodenaufruf über den Oberklassen-Typ
Der statische Typ von roman ist Buch. Zur Übersetzungszeit prüft der Compiler also: Gibt es in Buch eine Methode zeige() ? Dies ist der Fall, daher wird zur Laufzeit die zeige()-Methode von Roman aufgerufen, wenn es eine solche gibt. Falls Roman die Buch.zeige()-Methode nicht überschrieben hat, wird die Buch.zeige()-Methode ausgeführt.
Verletzungen des LSP
Codebeispiel
Dieses Beispiel stammt aus dem Buch-Projekt. Die Klasse Buch definiert als Oberklasse einige Methoden, unter anderem getAutor():
public String getAutor()
{
return autor;
}
Wenn eine Unterklasse wie Roman diese Methode überschreibt, erwartet man natürlich, dass sie ebenfalls den Autoren des Buches zurückliefert, vielleicht noch mit ein paar genaueren zusätzlichen Informationen. Ein solcher Entwurf entspräche der Semantik der Methode und damit den Erwartungen an ein Objekt der Klasse Buch.
Eine entsprechend implementierte getAutor()-Methode in der Klasse Roman würde daher das Liskovsche Substitutionsprinzip (LSP) erfüllen, da ein Roman-Objekt überall dort eingesetzt werden kann, wo ein Buch-Objekt erwartet wird
@Override
public String getAutor()
{
return super.getAutor() + " (Roman)";
}
Diese Roman-Methode erfüllt das LSP, weil sie den Erwartungen entspricht, die man an eine getAutor()-Methode hat.
Betrachten wir nun jedoch folgende Implementierung in der Klasse Roman:
@Override
public String getAutor()
{
return "Dieses Buch hat keinen Autor!;
}
Der Compiler beanstandet diesen Code nicht, da weder ein lexikalischer noch ein syntaktischer Fehler vorliegt. Auch zur Laufzeit tritt kein Fehler auf. Dennoch verletzt diese Implementierung das Liskovsche Substitutionsprinzip.
Der Grund dafür ist semantischer Natur: Die Methode getAutor() verspricht ihrem Namen und ihrer Definition in der Oberklasse zufolge, den Autor eines Buches als String zurückzugeben. Die gezeigte Implementierung bricht dieses implizite Versprechen, indem sie statt des tatsächlichen Autors eine inhaltlich widersprüchliche Aussage liefert.
Ein Objekt der Klasse Roman ist damit kein vollwertiger Ersatz für ein Objekt der Klasse Buch, obwohl die formale Methodensignatur korrekt eingehalten wird.
Hier noch ein Zitat aus [2]:
"[...] the Liskov Substitution principle says that if we create a class that extends another class or implements an interface, it has to behave as expected."
Das Fehlerbeispiel aus der Sicht der Design by Contract-Methode:
Vertrag der Oberklasse Buch
Die Oberklasse Book definiert einen Vertrag, an den sich alle Unterklassen halten müssen. Für die getAutor()-Methode der Klasse Buch gilt:
- Vorbedingungen: keine expliziten
- Nachbedingungen: Die Methode liefert den Namen des Autors als String zurück, der Rückgabewert ist nicht null.
- Invarianten: Während der gesamten Lebensdauer eines Buch-Objektes gilt: Jedes Buch besitzt einen Autor, und die Instanzvariable autor ist stets sinnvoll belegt, d.h. autor beschreibt den tatsächlichen Autoren des Buches.
Vertrag der Unterklasse Novel
- Vorbedingungen: keine expliziten. Die Unterklasse darf keine zusätzlichen Anforderungen stellen.
- Nachbedingungen: Die Nachbedingungen der Oberklasse werden vollständig erfüllt, zusätzliche Informationen zum Autor und/oder zum Buch sind allerdings erlaubt.
- Invarianten: Die Invariante "Jedes Buch hat einen Autor" der Oberklasse bleibt erhalten.
Bei unserem Fehler-Beispiel
@Override
public String getAutor()
{
return "Dieses Buch hat keinen Autor!;
}
findet bei den Nachbedingungen ein Vertragsbruch statt. Der Rückgabewert ist kein Autorenname, diese Nachbedingung der Oberklasse Buch wird also nicht erfüllt, und damit ist die Unterklasse kein vollwertiger Ersatz für die Oberklasse
Auch bei den Invarianten ("jedes Buch hat einen Autor") findet ein solcher Vertragsbruch statt.
Zusammenhang zwischen Design by Contract und LSP:
Eine Unterklasse darf Vorbedingungen nicht verschärfen, Nachbedingungen nicht abschwächen und Invarianten nicht verletzen!
Design by Contract (DbC) macht explizit, was das Liskovsche Substitutionsprinzip (LSP) implizit fordert: Eine Unterklasse muss den Vertrag seiner Oberklasse vollständig einhalten.
Quellen:
- Abts, D.: Grundkurs Java. 12. Auflage, Wiesbaden: Springer-Vieweg 2024.
- Noback, M.: Principles of Package Design. Boston: Addison-Wesley Professional, 2018.