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 
UserRepositoryzu 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.
