Ich habe gehört, dass das Liskov-Substitutionsprinzip (LSP) ein grundlegendes Prinzip des objektorientierten Designs ist. Was ist es und welche Beispiele gibt es für seine Anwendung?
Das Liskov-Substitutionsprinzip (LSP, [tag:lsp]) ist ein Konzept der objektorientierten Programmierung, das besagt:
Funktionen, die Zeiger oder Referenzen auf Basisklassen verwenden, müssen in der Lage sein, Objekte von abgeleiteten Klassen zu verwenden ohne es zu wissen.
Im Kern geht es bei LSP um Schnittstellen und Verträge sowie darum, wie man entscheidet, wann man eine Klasse erweitert und wann man eine andere Strategie wie Komposition verwendet, um sein Ziel zu erreichen.
Die effektivste Art, diesen Punkt zu illustrieren, habe ich in Head First OOA&D gesehen. Sie stellen ein Szenario vor, in dem Sie als Entwickler an einem Projekt zur Entwicklung eines Rahmens für Strategiespiele arbeiten.
Es wird eine Klasse vorgestellt, die ein Brett darstellt, das so aussieht:
Alle Methoden benötigen X- und Y-Koordinaten als Parameter, um die Position der Spielsteine im zweidimensionalen Array der "Tiles" zu bestimmen. Dies ermöglicht es dem Spieleentwickler, die Einheiten auf dem Spielbrett während des Spiels zu verwalten.
Im Buch werden die Anforderungen dahingehend geändert, dass das Spiel-Framework auch 3D-Spielbretter unterstützen muss, um Spiele mit Flug zu ermöglichen. Also wird eine Klasse ThreeDBoard
eingeführt, die Board
erweitert.
Auf den ersten Blick scheint dies eine gute Entscheidung zu sein. Board
bietet die Eigenschaften Height
und Width
und ThreeDBoard
bietet die Z-Achse.
Der Haken an der Sache ist, wenn man sich alle anderen von Board
geerbten Mitglieder ansieht. Die Methoden für AddUnit
, GetTile
, GetUnits
und so weiter, nehmen alle sowohl X- als auch Y-Parameter in der Klasse Board
, aber das ThreeDBoard
benötigt auch einen Z-Parameter.
Daher müssen Sie diese Methoden erneut mit einem Z-Parameter implementieren. Der Z-Parameter steht in keinem Zusammenhang mit der Klasse Board
und die geerbten Methoden der Klasse Board
verlieren ihre Bedeutung. Eine Codeeinheit, die versucht, die Klasse ThreeDBoard
als ihre Basisklasse Board
zu verwenden, hätte Pech gehabt.
Vielleicht sollten wir einen anderen Ansatz finden. Anstatt Board
zu erweitern, sollte ThreeDBoard
aus Board
-Objekten zusammengesetzt werden. Ein `Board'-Objekt pro Einheit der Z-Achse.
Dies erlaubt uns, gute objektorientierte Prinzipien wie Kapselung und Wiederverwendung zu verwenden und verletzt nicht LSP.
Funktionen, die Zeiger oder Verweise auf Basisklassen verwenden, müssen in der Lage sein, Objekte abgeleiteter Klassen zu verwenden, ohne dies zu wissen.
Als ich das erste Mal von LSP las, nahm ich an, dass dies in einem sehr strengen Sinne gemeint war und im Wesentlichen mit der Implementierung von Schnittstellen und typsicherem Casting gleichgesetzt wurde. Das würde bedeuten, dass LSP entweder durch die Sprache selbst gewährleistet ist oder nicht. In diesem strengen Sinne ist beispielsweise ThreeDBoard für den Compiler sicherlich durch Board ersetzbar.
Nachdem ich mich näher mit dem Konzept befasst habe, stellte ich jedoch fest, dass LSP im Allgemeinen weiter gefasst ist als dies.
Kurz gesagt, was es für den Client-Code bedeutet, zu "wissen", dass das Objekt hinter dem Zeiger von einem abgeleiteten Typ ist und nicht vom Typ des Zeigers, ist nicht auf die Typsicherheit beschränkt. Die Einhaltung von LSP kann auch durch die Untersuchung des tatsächlichen Verhaltens des Objekts getestet werden. Das heißt, man untersucht die Auswirkungen des Zustands eines Objekts und der Methodenargumente auf die Ergebnisse der Methodenaufrufe oder die Arten von Ausnahmen, die vom Objekt ausgelöst werden.
Um noch einmal auf das Beispiel zurückzukommen: Theoretisch können die Board-Methoden so eingerichtet werden, dass sie mit ThreeDBoard problemlos funktionieren. In der Praxis wird es jedoch sehr schwierig sein, Unterschiede im Verhalten zu verhindern, mit denen der Client möglicherweise nicht richtig umgehen kann, ohne die Funktionalität, die ThreeDBoard hinzufügen soll, zu behindern.
Mit diesem Wissen in der Hand kann die Bewertung der LSP-Treue ein großartiges Werkzeug sein, um zu bestimmen, wann Komposition der geeignetere Mechanismus für die Erweiterung bestehender Funktionalität ist, anstatt Vererbung.
Wäre die Implementierung von ThreeDBoard in Form eines Arrays von Board so nützlich?
Vielleicht möchten Sie Slices von ThreeDBoard in verschiedenen Ebenen als ein Board behandeln. In diesem Fall möchten Sie vielleicht eine Schnittstelle (oder abstrakte Klasse) für Board abstrahieren, um mehrere Implementierungen zu ermöglichen.
Was die externe Schnittstelle betrifft, so könnte man eine Board-Schnittstelle sowohl für TwoDBoard als auch für ThreeDBoard ausarbeiten (obwohl keine der oben genannten Methoden passt).