Liskov Substitution Principle



Definicja

Zasada podstawienia Liskov (ang. Liskov Substitution Principle, LSP) mówi, że obiekty klasy pochodnej powinny móc zastępować obiekty klasy bazowej bez zmiany poprawności działania programu.
Innymi słowy – jeśli klasa B dziedziczy po klasie A, to obiekt B powinien zachowywać się tak, by program, który działa z A, działał poprawnie także z B.

Przykład z życia

Wyobraźmy sobie system zarządzania prefabrykacją, który obsługuje różne typy elementów – belki, płyty i słupy.
Wszystkie mają wspólne cechy: wymiary, masę i funkcję transportu na plac budowy.

Jednak ktoś tworzy klasę „ElementTymczasowy”, która dziedziczy po „PrefabElement”, ale nie może być transportowana, bo to tylko element testowy (np. próbka).
Jeśli system spróbuje przetransportować każdy element w taki sam sposób, używając tej klasy, to program się „wywróci” — naruszy zasadę Liskov.

Każdy obiekt klasy pochodnej powinien zachowywać się spójnie z oczekiwaniami klasy bazowej.

Przykłady przed i po zastosowaniu zasady

PRZED

Poniższy przykład łamie zasadę Liskov, bo klasa TemporaryElement zmienia zachowanie metody transport, w sposób sprzeczny z klasą bazową.

Kod oczekuje, że każda instancja PrefabElement ma metodę transport, która działa poprawnie.
Jednak TemporaryElement zmienia jej zachowanie — powoduje wyjątek.
To łamie zasadę podstawienia Liskov, bo obiekt klasy pochodnej nie zachowuje się jak jego klasa bazowa.

class PrefabElement {
    protected String name;

    public PrefabElement(String name) {
        this.name = name;
    }

    public void transport() {
        System.out.println("Transporting " + name + " to construction site...");
    }
}


class TemporaryElement extends PrefabElement {
    public TemporaryElement(String name) {
        super(name);
    }


    @Override
    public void transport() {
        // Naruszenie LSP — ta klasa nie powinna być transportowana!
        throw new UnsupportedOperationException("Temporary elements cannot be transported!");
    }
}


public class Main {
    public static void transportElement(PrefabElement element) {
        element.transport();
    }


    public static void main(String[] args) {
        PrefabElement beam = new PrefabElement("Beam");
        TemporaryElement testSample = new TemporaryElement("Test Sample");

        transportElement(beam);          // OK
        transportElement(testSample);    // Błąd! Naruszenie LSP
    }
}
Designed by Freepik

PO

Zamiast łamać zasadę LSP, można przebudować hierarchię klas tak, by tylko transportowalne elementy dziedziczyły po PrefabElement, a inne implementowały inny interfejs lub klasę bazową.

Teraz klasy są zaprojektowane zgodnie z zasadą LSP:

  • PrefabElement można bezpiecznie przetransportować,
  • TemporaryElement istnieje niezależnie i nie psuje zachowania systemu.

Nie dochodzi do sytuacji, w której klasa pochodna łamie oczekiwania klasy bazowej.

abstract class Element {
    protected String name;

    public Element(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

abstract class TransportableElement extends Element {
    public TransportableElement(String name) {
        super(name);
    }

    public abstract void transport();
}

class PrefabElement extends TransportableElement {
    public PrefabElement(String name) {
        super(name);
    }

    @Override
    public void transport() {
        System.out.println("Transporting " + name + " to construction site...");
    }
}

class TemporaryElement extends Element {
    public TemporaryElement(String name) {
        super(name);
    }

    public void analyze() {
        System.out.println("Analyzing " + name + " in laboratory...");
    }
}

public class Main {
    public static void transportElement(TransportableElement element) {
        element.transport();
    }

    public static void main(String[] args) {
        PrefabElement beam = new PrefabElement("Beam");
        TemporaryElement sample = new TemporaryElement("Test Sample");

        transportElement(beam);      // OK
        // transportElement(sample); // Kompilator nie pozwoli – i o to chodzi!

        sample.analyze();            // Działa niezależnie
    }
}