Im fünften Wissenshäppchen geht es um das Liskov Substitution
Principle. TL;DR Am Liskov Substitution Principle (LSP) zeige ich
dir, worauf du beim Austauschen von Klassen achten musst:
Subklassen und Klassen, die ein Interface implementieren, müssen
das erwartete Verhalten auch zur Laufzeit einhalten. Unerwartete
Exceptions oder Seiteneffekte können bestehenden Code beschädigen,
obwohl der Compiler alle geforderten Methoden akzeptiert. An
Quadrat und Rechteck wird das greifbar; Unit-Tests helfen dir,
solche Fehler zu entdecken. Kleine, passende Interfaces erleichtern
es dir, den vereinbarten Vertrag vollständig zu erfüllen.
Kapitelmarken [00:00:20] Das dritte SOLID-Prinzip und Barbara
Liskov [00:01:11] Austauschbare Klassen und Polymorphie [00:02:01]
Was der Compiler bei Methoden prüft [00:03:38] Erwartetes Verhalten
zur Laufzeit [00:04:58] Definition des Liskov Substitution
Principle [00:06:51] LSP-Verletzung durch eine
NotImplementedException [00:09:09] Fehler mit Unit-Tests entdecken
[00:10:14] Quadrat und Rechteck als Vererbungsbeispiel [00:14:18]
Getrennte Klassen für Quadrat und Rechteck [00:15:07]
Ruby-Beispiel: Employee, Developer und Überstunden [00:17:42]
Prinzipien bewusst anwenden [00:18:13] Verständlicher Code durch
erwartbares Verhalten [00:19:12] Prinzip der geringsten
Überraschung [00:20:12] Einschränkungen bei der Vererbung
[00:21:19] Unveränderbare Listen und passende Abstraktionen
[00:23:02] Zusammenhang mit dem Interface Segregation Principle
[00:24:14] Rückblick auf das LSP [00:25:46] Shownotes,
Code-Beispiele und Buchempfehlung Inhalt Das LSP ist das dritte der
fünf SOLID-Prinzipien. Es wurde 1987 von Barbara Liskov definiert,
die ihm auch seinen Namen gab: Functions that use pointers or
references to base classes must be able to use objects of derived
classes without knowing it. Man könnte sagen, dass dies quasi die
Grundanforderung ist, um Polymorphie zu ermöglichen. Denn überall
dort, wo eine Basisklasse oder ein Interface erwartet wird, müssen
auch alle abgeleiteten bzw. implementierenden Klassen erlaubt sein.
Dies ist in den heutigen Programmiersprachen auch der Fall und der
Compiler prüft dies (statisch). Aber das LSP geht noch einen
Schritt weiter und fordert auch zur Laufzeit ein korrektes
Verhalten. Erklärung Subklassen müssen sich so verhalten, wie es
von ihren Basisklassen erwartet wird. Dies gilt auch für Interfaces
und ihre Implementierungen. Statisch ist das kein Problem: Der
Compiler prüft, ob alle geforderten Methoden von den Subklassen
bzw. implementierenden Klassen angeboten werden. Die korrekte
Verhaltensweise der Implementierung in den Subklassen bzw.
implementierenden Klassen kann jedoch nicht statisch geprüft
werden. Hierfür ist der Entwickler zuständig, um das LSP nicht zu
verletzen. Wenn beispielsweise eine Subklasse in einer
überschriebenen Methode eine NotImplementedException wirft, anstatt
sie zu implementieren, wird das LSP verletzt. Aber auch unerwartete
Nebeneffekte der Implementierungen in überschriebenen Methoden
verletzten das LSP (Beispiel: Square erbt von Rectangle und setzt
beim Aufruf von setA() auch die Seite b auf den gleichen Wert).
Beispiel Die Basisklasse Employee gibt die Methode work_overtime()
vor, die damit in allen Subklassen (Manager und Developer) zur
Verfügung steht. Somit kann das Project gerade noch erfolgreich
fertiggestellt werden. class Employee def work_overtime puts
"Working in the evening and on weekends..." end end class Manager
< Employee end class Developer < Employee end class Project
def finish(employees) employees.each { |e| e.work_overtime }
"Project done" end end puts Project.new.finish([Manager.new,
Developer.new]) # Working in the evening and on weekends... #
Working in the evening and on weekends... # Project done Aber was
passiert, wenn sich der Developer zwar formell an den "Vertrag"
seiner Basisklasse hält, die Methode work_overtime() aber wie folgt
überschreibt? class Developer < Employee def work_overtime raise
"Sorry, I use XP and adhere to the 40 hour workweek" end end puts
Project.new.finish([Manager.new, Developer.new]) # Working in the
evening and on weekends... # Traceback (most recent call last): #
4: from ./LSP.rb:23:in `' # 3: from ./LSP.rb:18:in `finish' # 2:
from ./LSP.rb:18:in `each' # 1: from ./LSP.rb:18:in `block in
finish' # ./LSP.rb:12:in `work_overtime': Sorry, I use XP and
adhere to the 40 hour workweek (RuntimeError) Autsch! Das Project
ist wohl zum Scheitern verurteilt! ;-) Es hatte ja keine Ahnung,
dass man Developer nicht zu Überstunden verdonnern kann, denn das
geht ja schließlich bei allen Employees... Vorteile Der Code wird
deutlich besser nachvollziehbar, da keine unerwarteten Fehler zur
Laufzeit auftreten, obwohl laut Compiler alles ok ist. Klassen
verhalten sich so, wie man es von ihnen erwarten würde ("Principle
of Least Astonishment"), und überraschen den Entwickler nicht.
Nachteile Die Möglichkeiten der Vererbung werden ein wenig
eingeschränkt. Literaturempfehlungen Auch beim dritten
SOLID-Prinzip empfehle ich wieder Uncle Bobs Clean Code*. * Links
Permalink zu dieser Podcast-Episode RSS-Feed des Podcasts Liskov
Substitution Principle ArticleS.UncleBob.PrinciplesOfOod (Artikel
von Uncle Bob, Beispiel in C++) Liskovsches Substitutionsprinzip –
Wikipedia Zusammenfassung Was das Liskov Substitution Principle
verlangt Mit dem Liskov Substitution Principle (LSP) behandle ich
das dritte SOLID-Prinzip. Es wurde 1987 von der Entwicklerin
Barbara Liskov definiert und trägt ihren Namen. "Substitution"
bedeutet "Ersatz". Wo dein Code eine Basisklasse erwartet, muss er
auch mit einem Objekt einer abgeleiteten Klasse korrekt arbeiten
können. Dasselbe gilt für Interfaces und die Klassen, die sie
implementieren. Der aufrufende Code soll dabei unabhängig davon
bleiben, welche konkrete Klasse er erhält. Für dich als IT-Azubi
gehört dieser Zusammenhang zur Polymorphie, die auch in der
Abschlussprüfung vorkommt. Das LSP verlangt, dass die
austauschbaren Objekte das erwartete Verhalten zur Laufzeit
einhalten. Was der Compiler prüft In den betrachteten Sprachen Java
und C# prüft der Compiler, ob eine konkrete Klasse die geforderten
abstrakten Methoden mit passenden Signaturen implementiert. Fehlen
solche Methoden, muss die Klasse selbst abstrakt bleiben oder der
Code lässt sich nicht kompilieren. Ob eine Methode fachlich das
Erwartete tut, liegt bei dir als Entwickler:in. Eine vorhandene
Methode kann zur Laufzeit unerwartete Seiteneffekte auslösen oder
eine Exception werfen, die der aufrufende Code gar nicht
berücksichtigt. Beispiel: Eine Methode wirft eine Exception In
meinem ersten Beispiel überschreibe ich eine abstrakte Methode und
hinterlege darin lediglich eine NotImplementedException. Damit ist
die Methode für den Compiler vorhanden. Beim Aufrufen erwartet der
bestehende Code, dass die vereinbarte Aufgabe ausgeführt wird.
Durch die unerwartete Exception kann das Programm abbrechen, wenn
eine passende Fehlerbehandlung fehlt. Die Subklasse verletzt damit
das LSP. Mit Unit-Tests kannst du solche Fehler entdecken. Ohne
Tests zeigt sich das Problem möglicherweise erst beim Ausführen des
Programms, wodurch es aufwendiger und teurer wird, den Fehler zu
beheben. Michael Feathers bezeichnet Code ohne Unit-Tests als
Legacy-Code. Beispiel: Quadrat und Rechteck Für das geometrische
Beispiel lasse ich Square von Rectangle erben. Die Klasse Rectangle
bietet mit SetA und SetB zwei Setter an, über die sich die beiden
Seitenlängen unabhängig voneinander ändern lassen. Ein Quadrat muss
gleich lange Seiten behalten. Seine Setter ändern deshalb jeweils
beide Werte. Wenn du SetA aufrufst, wird auch B angepasst. Code,
der mit Rectangle arbeitet, erwartet beim Setzen von A, dass B
unverändert bleibt. Ein entsprechender Unit-Test schlägt mit einem
Square fehl. Auch produktiver Code kann durch diesen Seiteneffekt
falsche Ergebnisse liefern. Für dieses Modell empfehle ich
getrennte Klassen für Quadrat und Rechteck, gegebenenfalls mit
einer gemeinsamen Basisklasse für geometrische Formen. Eine direkte
Vererbungsbeziehung in umgekehrter Richtung passt ebenfalls nicht
zu dem beschriebenen Verhalten. Beispiel: Überstunden im
Klassenmodell Im Ruby-Beispiel aus meinen Shownotes erben Manager
und Developer von Employee. Über WorkOvertime kann der aufrufende
Code Überstunden für alle Mitarbeiter:innen anfordern. Die Klasse
Developer folgt im Beispiel der 40-Stunden-Woche aus Extreme
Programming und verweigert die Methode mit einer Exception. Dadurch
verletzt sie das Verhalten, das der Code von Employee erwartet.
Verständlicher Code und weniger Überraschungen Wenn ich mich auf
den Vertrag einer Basisklasse oder eines Interfaces verlassen kann,
lässt sich mein Code leichter nachvollziehen. Besondere Regeln
einzelner Subklassen muss ich dann beim Aufrufen nicht
berücksichtigen. Dazu passt das Principle of Least Astonishment,
das Prinzip der geringsten Überraschung. Verständlicher Code und
erwartbares Verhalten verringern die Wahrscheinlichkeit von
Fehlern. Das LSP begrenzt, welches Verhalten du in einer Subklasse
verändern darfst. SOLID-Prinzipien verlangen grundsätzlich eine
bewusste Entscheidung über ihren Einsatz und den damit verbundenen
Aufwand. Beim LSP gewichte ich die Vorteile höher als diese
Einschränkung. Passende Interfaces wählen An einer unveränderbaren
Liste beschreibe ich ein weiteres Problem: Im Beispiel erwartet der
aufrufende Code, über das gewählte Listen-Interface Elemente
löschen zu können....
Kommentare (0)
Melde dich an, um einen Kommentar zu schreiben.