Das Liskovskische Substitutionsprinzip

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert