Das Liskovskische Substitutionsprinzip (LSP) wurde 1987 von Barbara Liskov beschrieben und ist ein Kriterium in der objektorientierten Programmierung, das die Bedingungen zur Modellierung eines Datentyps für seinen Untertyp angibt. Es besagt, dass ein Programm, das Objekte einer Basisklasse T verwendet, auch mit Objekten der davon abgeleiteten Klasse S korrekt funktionieren muss, ohne dabei das Programm zu verändern.
Oft wird verkürzt beschrieben, dass eine Klasse S von Klasse T erben darf, sofern es korrekt ist zu sagen Klasse S ist eine Klasse T. Dabei ist die viel wichtigere Frage, ob Klasse S sich genauso wie Klasse T verhält. Es darf auf die Arbeitsweise der Konsumenten der Klasse T keine Auswirkungen haben, wenn man Klasse T mit Klasse S austauscht.
Beispiel
public class UserRepository { public User GetUserById(int id) { return _context.Users.Find(id); } }
Das o.g. UserRepository hat eine Methode zur Rückgabe eines Users anhand seiner ID und gibt den Request an eine Datenbank weiter.
Wird der User nicht gefunden, wird die Methode null zurückgeben.
Dieses Verhalten erwartet jeder Konsument, weil es so irgendwann einmal definiert wurde. Eine abgeleitete Klasse darf dieses Verhalten nicht ändern, sie darf es nur erweitern und zwar dann, wenn ihre Aufgabe identisch mit der des UserRepository ist: Anfragen an eine Datenbank kapseln und zur einfachen Verwendung zur Verfügung stellen. Es ist nicht erlaubt eine andere Datenquelle in einer erbenden Klasse zu nutzen und die erbende Klasse darf auch nicht eine Exception bei einer eigentlichen Null-Rückgabe schmeißen.
public class ExtendedUserRepository : UserRepository { public User GetUserByName(string name) { return _context.Users.FirstOrDefault(user => user.Name == name); } }
Diese Erweiterung ist dagegen erlaubt. Das eigentliche UserRepository hatte keine Möglichkeit einen Benutzer anhand seines Namens zu erhalten. Diese Funktionalität wurde implementiert, dabei hat sich bei einer Ersetzung der alte Code nicht verändert und neuer Code kann auf die neue Funktionalität zugreifen. Die Aufgabe der Klasse bleibt identisch.
Grade zu Beginn wird Vererbung aber gerne dafür verwendet Funktionalität zur Verfügung zu stellen ohne diese mehrfach schreiben zu müssen:
public class ConsolePrint { protected void Print(string message) => Console.WriteLine(message); } public class UserRepository : ConsolePrint { public void GetUserById(int id) { Print($"Looking for user with id {id}"); var user = _context.Users.Find(id); if(user == null) Print("User not found"); else Print("User found"); return user; { }
- Die Aufgabe von PrintBase ist nicht identisch mit UserRepository und stellt in diesem Fall sogar eine ganz andere Funktionalität zur Verfügung.
- GetUserById macht mehr als nur den Nutzer anhand der ID zu ermitteln.
- Eine Änderung der Log Ausgabe wie z.B. das Schreiben in eine Datei ist im Nachhinein nur durch Änderung der Basisklasse möglich.
- Diese Funktionalität hätte besser über Komposition zur Verfügung gestellt werden sollen.