Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.
Wann kommt nun der gemeine Programmierer mit dem Liskovschen Substitutionsprinzip in Berührung? Fragte man ihn, würde er müde mit dem Kopf schütteln und bestreiten, jemals etwas mit so einem Konstrukt zu tun gehabt zu haben. In Wirklichkeit hat er es aber wahrscheinlich einfach nur nicht gemerkt.
Was genau sagt das Substitutionsprinzip jetzt eigentlich aus?
Kurz zusammengefasst lässt es sich so sagen: Jeder Untertyp muss alle Funktionalitäten erhalten, die bereits der Obertyp bereitgestellt hat. Er kann also keine Fähigkeiten und Eigenschaften verlieren, sondern ausschließlich dazulernen. Sprechen wir nun über die Javawelt, fordert Frau Liskov also, dass jede Unterklasse mindestens die Methoden (strenger: Funktionalitäten) ihrer Elternklasse bereitstellen muss.
Wieso sollte man sowas tun?
Auch in der Welt der Javaprogramme ist es so, dass nicht immer alles gehalten wird, was man so verspricht. Die Möglichkeit, an die Stelle einer Klasse auch immer Instanzen von Subklassen treten zu lassen, zwingt uns praktisch dazu, uns im Codegerüst an das Substitutionsprinzip zu halten. Sähe unser Verstand vor, dass man mit einem Hund spazierengehen muss, käme es sicher zu einem Fehler, wenn wir dies mit einer Subklasse Chihuahua plötzlich gar nicht mehr ginge.
Und es geht gar nicht ohne?
Der B.Sc. würde sagen: Alles kann, nichts muss. Das Substitutionsprinzip ist - ja richtig - ein Prinzip, kein Gesetz. Nur eine strikte Vererbung würde die Umsetzung wirklich erzwingen können. In Java können wir durch Überschreiben die internen Funktionalitäten immer ändern: Gilt "Beim Spazierengehen entleert der Hund seine Blase" (schöner Euphemismus) für alle Hunde, können wir dennoch das Spazierengehen beim Chihuahua so gestalten, dass er nur bellt und nicht pinkelt.
Anders ist es mit den "Grundfunktionen", die ein Objekt zur Verfügung stellt, also den definierten Operationsschnittstellen. Ich kann in Java zwar, wie schon gesagt, interne Funktionalitäten entfernen, nicht aber die öffentlichen Deklarationen (um im Beispiel zu bleiben: auch wenn er nicht pinkelt, Spazierengehen muss man auch mit einem Chihuahua können). Eine einmal bereitgestellte Methodensignatur muss auch in allen Unterklassen des Objekts vorhanden sein.
Was sind Kovarianz, Kontravarianz und Invarianz?
Im Zusammenhang mit dem Substitutionsprinzip betrachtet man meistens die Varianzen in Eingabe und Rückgabe von Methoden. Was ist nun die genaue Bedeutung dieser Varianztypen?
Kovarianz bedeutet, dass sich ein Eingabeparameter (oder die Rückgabe) der Methode mit der Vererbungshierarchie ändert. Anders gesagt: Betrachte ich eine Subklasse meiner Klasse, so ist auch der Parameter eine Subklasse meines Eingabeparameters.
Kontravarianz ist das genau Gegenteil. Betrachte ich wieder den Eingabeparameter einer Methode, so ist dieser kontravariant, wenn in meiner Subklasse als Eingabeparameter eine Oberklasse meines Eingabeparameters benutzt wird. Für die Rückgabetypen gilt es analog.
Invarianz ist der einfachste Fall. Der Rückgabetyp (oder Typ des Parameters) einer Methode bleibt auch in der Subklasse exakt der gleiche.
Wo ist der Bezug zum Substitutionsprinzip?
Mit Hilfe der Varianzen lässt sich beschreiben, wie sich Eingabe und Rückgabe einer überschriebenen Methode ändern dürfen, ohne das Substitutionsprinzip zu verletzen, wenn wir Klassen ableiten. Es gilt dabei immer zu beachten, dass die Subklasse mit ihrer Methode genauso eingesetzt werden soll wie die Oberklasse.
Zuerst die Eingabeparameter: Bleibt der Parametertyp unverändert, ist natürlich alles in Ordnung, denn die Methode der Unterklasse kann exakt das verarbeiten, womit schon die Oberklasse umgehen konnte. Bei Kontravarianz ist dies ähnlich, hier hat man sogar einen Funktionszuwachs. Konnte die Methode zuvor nur eine bestimmte Klasse verarbeiten, kommen jetzt außerdem die Oberklasse und alle Schwesterklassen in Frage. Anders ist es bei der Kovarianz: Konnte ich zuvor alle Objekte einer bestimmten Klasse übergeben, ist es mir jetzt nur noch möglich, die Objekte einer bestimmten Subklasse bearbeiten zu lassen. Das würde das Substitutionsprinzip verletzen.
Beim Return-Typ verhält es sich ein wenig anders. Auch hier muss die Invarianz nicht näher betrachtet werden. Kovarianz und Kontravarianz hingegen vertauschen ihre Rollen. Hat mir die Methode der Oberklasse ein Objekt einer bestimmten Klasse zurückgegeben, bekomme ich bei der Kontravarianz nur noch ein allgemeineres Objekt. Wenn ich nun weiterarbeiten möchte, könnten mir plötzlich Methoden fehlen, die früher wie selbstverständlich vorhanden waren. Bei Kovarianz dagegen bekomme ich eine Unterklasse des ursprünglichen Rückgabetyps. Das bedeutet, alle ursprünglichen Methoden sind vorhanden, mit etwas Glück bekomme ich sogar ein Objekt, das noch viel mehr kann als zuvor. Kovarianz in der Rückgabe erfüllt also das Substitutionsprinzip.
Unterstützt denn Java das Substitutionsprinzip bei Ein- und Rückgabe?
Auf jeden Fall. Java ist bei den Eingabeparametern allerdings noch strenger als nötig. Eine Methode wird nur dann wirklich überschrieben, wenn die Methoden-Parameter in der Subklasse exakt mit denen in der Oberklasse übereinstimmen. Bei Kontravarianz liegt für Java eine unterschiedliche Signatur vor, weshalb die Methode nur überladen wird.
