Liskov Substitution

Liskov Substitution

Das Liskov Substitution Principle (LSP) ist eines der fünf Prinzipien der objektorientierten Programmierung, die als SOLID bekannt sind. Es wurde von Barbara Liskov 1987 formuliert und besagt:

„Wenn eine Klasse S eine Unterklasse von T ist, sollte es möglich sein, Objekte der Klasse T durch Objekte der Klasse S zu ersetzen, ohne dass sich das korrekte Verhalten des Programms ändert.“

Mit anderen Worten: Wenn du eine Instanz einer Basisklasse (Superklasse) durch eine Instanz einer abgeleiteten Klasse (Subklasse) ersetzt, sollte das Programm weiterhin korrekt funktionieren, ohne dass Fehler oder unerwartetes Verhalten auftreten.

Wichtigste Aspekte des Liskov Substitution Principle:

  1. Verhalten muss konsistent bleiben: Wenn du eine Subklasse verwendest, sollte sich das Verhalten des Programms nicht ändern, und die Subklasse sollte alle Eigenschaften der Basisklasse korrekt vererben. Die Subklasse sollte das Verhalten der Basisklasse erweitern oder anpassen, aber niemals die Funktionsweise der Basisklasse verletzen.
  2. Vertragsbeziehungen beibehalten: Eine Subklasse sollte alle vertraglichen Vereinbarungen der Basisklasse einhalten, die in der Basisklasse definiert sind. Das bedeutet, dass Vorbedingungen (Bedingungen, die erfüllt sein müssen, bevor eine Methode ausgeführt wird) und Nachbedingungen (Bedingungen, die nach der Ausführung der Methode erfüllt sein müssen) in der Subklasse nicht strenger werden dürfen als in der Basisklasse.
  3. Keine Einschränkung der Erwartungen: Die Subklasse sollte nicht die Erwartungen des Clients an die Basisklasse einschränken. Das heißt, wenn ein Benutzer der Basisklasse eine bestimmte Funktionalität erwartet, sollte die Subklasse diese Funktionalität genauso oder besser bereitstellen.

Beispiel: Liskov Substitution in C++

Das Liskov Substitution Principle (LSP) in C++ zu implementieren bedeutet, sicherzustellen, dass Instanzen von Unterklassen überall dort verwendet werden können, wo Instanzen der Basisklasse erwartet werden, ohne das korrekte Verhalten des Programms zu beeinträchtigen.

Hier ist ein einfaches Beispiel in C++, das das Liskov Substitution Principle veranschaulicht:

Beispiel: Tiere und Bewegungslogik

Stellen wir uns vor, wir haben eine Basisklasse Animal und zwei abgeleitete Klassen: Bird und Penguin. Bird kann fliegen, während Penguin nicht fliegen kann. Wir wollen das Liskov Substitution Principle beibehalten, sodass wir eine Methode haben, die mit Animal-Objekten arbeitet, aber trotzdem korrekt funktioniert, wenn sie mit Bird oder Penguin-Instanzen arbeitet.

Beispielcode in C++:

#include <iostream>
using namespace std;

// Basisklasse: Animal
class Animal {
public:
    virtual void move() const {
        cout << "Das Tier bewegt sich." << endl;
    }
    
    virtual ~Animal() {}  // Virtueller Destruktor
};

// Subklasse: Bird (Vogel)
class Bird : public Animal {
public:
    void move() const override {
        cout << "Der Vogel fliegt." << endl;
    }
};

// Subklasse: Penguin (Pinguin)
class Penguin : public Animal {
public:
    void move() const override {
        cout << "Der Pinguin läuft." << endl;
    }
};

// Funktion, die mit Tieren arbeitet und deren move() Methode aufruft
void makeAnimalMove(const Animal& animal) {
    animal.move();  // Diese Methode wird die move()-Methode der richtigen Subklasse aufrufen
}

int main() {
    Bird bird;
    Penguin penguin;

    makeAnimalMove(bird);     // Ausgabe: Der Vogel fliegt.
    makeAnimalMove(penguin);  // Ausgabe: Der Pinguin läuft.

    return 0;
}

Erklärung:

  1. Basisklasse Animal:
    • Sie enthält die Methode move(), die das allgemeine Verhalten des Tiers beschreibt.
    • Der Destruktor ist virtuell, um eine korrekte Vererbung und das richtige Löschen von abgeleiteten Klassenobjekten zu gewährleisten.
  2. Subklasse Bird:
    • Diese Klasse überschreibt die move()-Methode der Basisklasse, um das Verhalten eines fliegenden Vogels zu implementieren.
  3. Subklasse Penguin:
    • Diese Klasse überschreibt ebenfalls die move()-Methode der Basisklasse, aber auf eine andere Weise, da Pinguine nicht fliegen können, sondern laufen.
  4. Funktion makeAnimalMove:
    • Diese Funktion akzeptiert ein Animal-Objekt (oder jedes Objekt, das von Animal erbt) und ruft dessen move()-Methode auf.
    • Dank der polymorphen Funktionsaufrufmechanik (mit virtuellen Methoden) wird je nach Typ des übergebenen Objekts die richtige move()-Methode (von Bird oder Penguin) aufgerufen.

Wie erfüllt dieses Beispiel das Liskov Substitution Principle?

  • Polymorphismus: Da sowohl Bird als auch Penguin von der Basisklasse Animal erben und die move()-Methode überschreiben, können wir beide Subklassen in der Funktion makeAnimalMove verwenden, ohne dass sich das Verhalten des Programms ändert.
  • Erweiterbarkeit: Wenn wir eine neue Tierklasse wie Fish hinzufügen, die ebenfalls von Animal erbt und eine move()-Methode implementiert, können wir sie ohne Probleme in die makeAnimalMove-Funktion integrieren, ohne den bestehenden Code zu ändern.
  • Keine Einschränkung des Verhaltens: In der Subklasse Penguin haben wir das Verhalten von move() so angepasst, dass es für einen Pinguin sinnvoll ist (Laufen statt Fliegen). Die Klasse Penguin erfüllt weiterhin die Erwartungen an das move()-Verhalten, sodass das Programm weiterhin korrekt funktioniert, wenn ein Penguin-Objekt über die Animal-Referenz behandelt wird.

Problem, wenn das Liskov Substitution Principle verletzt wird

Wenn wir die Liskov Substitution nicht befolgen, könnte dies zu Fehlern oder unerwartetem Verhalten führen. Zum Beispiel könnte die Penguin-Klasse die Methode move() überschreiben und einen Fehler werfen (wie im vorherigen Python-Beispiel), was das gesamte Programm zum Absturz bringen könnte.

Beispiel einer Verletzung des LSP:

// Subklasse Penguin, die das LSP verletzt
class Penguin : public Animal {
public:
    void move() const override {
        throw "Pinguine können nicht fliegen!" ;  // Ein Fehler wird geworfen, was das LSP verletzt.
    }
};

int main() {
    Penguin penguin;
    makeAnimalMove(penguin);  // Dies wird einen Fehler auslösen und das Programm zum Absturz bringen
}

In diesem Fall würde das Ersetzen eines Animal-Objekts durch ein Penguin-Objekt die korrekte Funktionsweise des Programms verhindern, was das Liskov Substitution Principle verletzt.

Das Liskov Substitution Principle in C++ fordert, dass Unterklassen die Funktionalität ihrer Basisklassen erweitern, ohne deren Verhalten zu zerstören. In unserem Beispiel funktioniert das Programm korrekt, wenn wir Objekte der Subklassen Bird und Penguin an die Funktion makeAnimalMove übergeben, da alle Subklassen eine gültige Implementierung der Methode move() bieten, die den Erwartungen der Basisklasse entspricht.

Beispiel: Liskov Substitution in Python

Stellen wir uns vor, wir haben eine Basisklasse Bird und zwei Subklassen: Sparrow und Penguin.

Code:

# Basisklasse: Vogel
class Bird:
    def fly(self):
        print("Vogel fliegt.")

# Subklasse: Sperling (kann fliegen)
class Sparrow(Bird):
    def fly(self):
        print("Sperling fliegt.")

# Subklasse: Pinguin (kann nicht fliegen)
class Penguin(Bird):
    def fly(self):
        raise NotImplementedError("Pinguine können nicht fliegen.")

# Funktion, die mit einer Liste von Vögeln arbeitet
def make_bird_fly(bird: Bird):
    bird.fly()

# Verwendung:
sparrow = Sparrow()
penguin = Penguin()

make_bird_fly(sparrow)  # Sollte funktionieren
make_bird_fly(penguin)  # Wird einen Fehler werfen

Problem mit Liskov Substitution:

In diesem Beispiel verletzen wir das Liskov Substitution Principle. Die Subklasse Penguin überschreibt die Methode fly(), aber sie wirft einen Fehler, weil Pinguine nicht fliegen können. Dies führt dazu, dass der Client, der erwartet, dass make_bird_fly() mit einem Bird-Objekt arbeitet, in einer Situation landet, in der das Programm fehlschlägt (da der Pinguin nicht fliegen kann).

Korrektur nach dem Liskov Substitution Principle:

Um das Liskov Substitution Principle zu befolgen, könnten wir die Basisklasse Bird so gestalten, dass sie eine allgemeinere Methode für Vögel bietet, die fliegen können, und eine für Vögel, die nicht fliegen können. Alternativ könnten wir eine FlyingBird-Klasse erstellen, die nur fliegende Vögel repräsentiert.

Code nach der Korrektur:

# Basisklasse: Vogel
class Bird:
    def move(self):
        pass  # Vögel bewegen sich irgendwie, aber nicht unbedingt durch Fliegen

# Subklasse: Fliegender Vogel
class FlyingBird(Bird):
    def move(self):
        print("Fliegender Vogel fliegt.")

# Subklasse: Sperling (fliegend)
class Sparrow(FlyingBird):
    def move(self):
        print("Sperling fliegt.")

# Subklasse: Pinguin (nicht fliegend)
class Penguin(Bird):
    def move(self):
        print("Pinguin läuft.")

# Funktion, die mit einer Liste von Vögeln arbeitet
def make_bird_move(bird: Bird):
    bird.move()

# Verwendung:
sparrow = Sparrow()
penguin = Penguin()

make_bird_move(sparrow)  # Sollte funktionieren
make_bird_move(penguin)  # Sollte auch funktionieren, aber mit anderem Verhalten

Erklärung:

  • Jetzt haben wir eine Bird-Basisklasse, die eine move()-Methode definiert. Diese Methode wird von Penguin und Sparrow überschrieben.
  • Für fliegende Vögel haben wir eine separate FlyingBird-Klasse erstellt, die die move()-Methode so implementiert, dass sie das Fliegen simuliert. Diese Klasse wird dann von der Sparrow-Klasse geerbt.
  • Jetzt respektiert das Programm das Liskov Substitution Principle, da beide Klassen (Penguin und Sparrow) korrekt als Bird-Instanzen behandelt werden können, ohne dass die Funktionsweise des Programms fehlschlägt.

Wann sollte man das Liskov Substitution Principle beachten?

  • Polymorphismus und Vererbung: Wenn du Polymorphismus in deinem Code verwendest, um mit Objekten unterschiedlicher Klassen auf eine einheitliche Weise zu arbeiten, solltest du sicherstellen, dass alle Unterklassen das Verhalten ihrer Basisklassen korrekt implementieren und keine unvorhergesehenen Ausnahmen oder Fehler einführen.
  • Erweiterbarkeit und Wartbarkeit: Bei der Gestaltung von Software, die leicht erweiterbar und wartbar sein soll, hilft das Liskov Substitution Principle, unerwünschte Abhängigkeiten zu vermeiden und sicherzustellen, dass Änderungen in der Basisklasse keine unerwünschten Nebeneffekte haben.

Fazit

Das Liskov Substitution Principle (LSP) fordert, dass Objekte einer Subklasse ohne Fehlverhalten überall dort eingesetzt werden können, wo Objekte der Basisklasse verwendet werden. Dadurch wird die Integrität des Systems gewahrt, und die Erweiterbarkeit wird unterstützt, indem sicher gestellt wird, dass neue Klassen in bestehende Systeme integriert werden können, ohne dass diese das Verhalten unerwartet ändern.

Zur Liste der Pattern: Liste der Design-Pattern

VG WORT Pixel

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)