Das Liskovsche Substitutionsprinzip (LSP) wurde 1987 von Barbara Liskov formuliert und ist ein zentrales Prinzip objektorientierten Designs. Es besagt: Wenn ein Programm korrekt mit einer Basisklasse T arbeitet, muss es auch korrekt funktionieren, wenn T durch eine Unterklasse S ersetzt wird – ohne dass sich das Verhalten des Programms ändert.
Oft wird das Prinzip vereinfacht mit „S ist ein T“ beschrieben. Doch entscheidend ist nicht die formale Ableitung, sondern das Verhalten: Eine abgeleitete Klasse darf die Erwartungen an die Basisklasse nicht verletzen. Konsumenten dürfen keinen Unterschied merken – sonst ist die Substitution unzulässig.
Beispiel: Repository mit konsistentem Verhalten
Ein einfaches Repository, das Benutzer anhand ihrer ID aus einer Datenbank liefert, könnte so aussehen:
public class UserRepository { public User GetUserById(int id) { return _context.Users.Find(id); // Gibt null zurück, wenn nicht gefunden } }
Konsumenten dieser Klasse erwarten, dass bei unbekannter ID null
zurückgegeben wird. Eine Unterklasse darf dieses Verhalten nicht stillschweigend verändern – z. B. durch eine Exception oder durch das Verwenden einer anderen Datenquelle.
Erlaubt ist jedoch eine Erweiterung, die zusätzliche Funktionalität bietet, das bestehende Verhalten aber nicht verändert:
public class ExtendedUserRepository : UserRepository { public User GetUserByName(string name) { return _context.Users.FirstOrDefault(u => u.Name == name); } }
Die ursprüngliche Methode GetUserById
bleibt unverändert. Der bestehende Code funktioniert weiter wie zuvor. Neue Funktionalität kann genutzt werden, muss aber nicht – das ist mit dem LSP vereinbar.
Verletzung des LSP: Vererbung aus Zweckmäßigkeit
Ein häufiger Fehler in der Praxis ist Vererbung zur Wiederverwendung von Funktionalität – etwa für Logging-Ausgaben:
public class ConsolePrint { protected void Print(string message) => Console.WriteLine(message); } public class UserRepository : ConsolePrint { public User 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; } }
- ConsolePrint hat nichts mit der eigentlichen Aufgabe von
UserRepository
zu tun. - Die Methode
GetUserById
übernimmt nun auch Logging – und verletzt das Single-Responsibility-Prinzip. - Das Verhalten ist an eine bestimmte Ausgabeart gekoppelt. Änderungen erfordern Eingriffe in die Basisklasse.
Besser: Komposition mit Abhängigkeit
Statt die Basisklasse nur für Logging zu missbrauchen, sollte die benötigte Funktionalität explizit per Abhängigkeit eingebunden werden – z. B. über ein Logging-Interface:
public class UserRepository { private readonly ILogger<UserRepository> _logger; public UserRepository(ILogger<UserRepository> logger) { _logger = logger; } public User GetUserById(int id) { _logger.LogInformation($"Looking for user with id {id}"); var user = _context.Users.Find(id); return user; } }
So bleibt die Klasse offen für Erweiterung, aber geschlossen für Veränderung – und das Verhalten ist vollständig kontrollierbar.
LSP-konform entwickeln – kurze Checkliste
- Gibt die abgeleitete Klasse dieselben Ergebnisse für dieselben Eingaben zurück?
- Wirft sie keine neuen Exceptions, die die Basisklasse nicht geworfen hätte?
- Verwendet sie keine anderen Abhängigkeiten oder Datenquellen?
- Bleiben Vor- und Nachbedingungen (z. B. Rückgabewerte) konsistent?
- Kann man die Basisklasse 1:1 durch die abgeleitete Klasse ersetzen?
Wenn auch nur eine dieser Fragen mit „nein“ beantwortet wird, sollte die Funktionalität durch Komposition statt durch Vererbung realisiert werden.