Kluczowa zasada do zapewnienia elastyczności i rozszerzalności kodu. Umożliwia tworzenie hierarchii klas, które mogą efektywnie współpracować i być rozbudowywane.

Definicja

Każda klasa pochodna powinna być zastępowalna przez swoją klasę bazową bez naruszania poprawności programu.

Oznacza to, że klasa pochodna powinna zachowywać się tak samo jak klasa bazowa i nie wprowadzać żadnych nieoczekiwanych zmian lub ograniczeń. Dodatkowo warunki wstępne w podklasie nie mogą być trudniejsze do spełnienia niż w klasie po której dziedziczy, a warunki końcowe w podklasie nie mogą być łatwiejsze do spełnienia niż w klasie w bazowej. Jeśli nie klasa pochodna jest zastępowalna przez swoją klasę bazową, to łamie zasadę LSP i powoduje problemy z polimorfizmem, dziedziczeniem i abstrakcją.

Przykład

Stwórzmy klasę Bird ze wspólnymi cechami ptaków, w tym metodą fly(), która będzie rozszerzać klasy Sparrow i Penguin.

public class Bird {
    //Wspólne cechy ptaków
    String name;
    public void fly(){
        System.out.println(name + " leci");
    }
} 
public class Sparrow extends Bird {
    public Sparrow() {
        this.name = "Elemelek";
    }
} 
public class Penguin extends Bird{
    public Penguin() {
        this.name = "Jakub";
    }

    @Override
    public void fly() {
        throw new UnsupportedOperationException("Pingwin nie potrafi latać");
    }
} 

Klasa ta łamie zasadę LSP, ponieważ klasa Penguin zmienia zachowanie metody fly() i wyrzuca wyjątek, przez co nie jest zastępowalna przez swoją klasę bazową Bird.

Aby naprawić ten kod nalezy zmienić hierarchię klas i wydzielić wspólne cechy ptaków do interfejsów lub klas abstrakcyjnych. W ten sposób klasy pochodne będą implementować tylko te metody, które są dla nich odpowiednie i nie będą naruszać kontraktu klasy bazowej.

W poprawionym kodzie dodałam interfejs FlyingBird do której przeniosłam metodę fly(). Z kolei w klasie Bird dodałam metodę eat(), która będzie wspólna dla wszystkich obiektów dziedziczących po niej

public class Bird {
    //Wspólne cechy ptaków
    String name;
    public Bird(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println(name + " je.");
    }
} 
public interface FlyingBird{
    public void fly();
} 
public class Sparrow extends Bird implements FlyingBird {
    public Sparrow(String name) {
        super(name);
    }

    @Override
    public void fly() {
        System.out.println(this.name + " leci");
    }
} 
public class Penguin extends Bird{
    public Penguin(String name) {
        super(name);
    }
} 

Przykład wywołania:

public class App
{
    public static void main( String[] args )
    {
        Sparrow sparrow = new Sparrow("Elemelek");
        Penguin penguin = new Penguin("Jakub");

        List<Bird> birdList = new ArrayList<>();
        birdList.add(sparrow);
        birdList.add(penguin);

        List<FlyingBird> flyingBirds = new ArrayList<>();
        flyingBirds.add(sparrow);
        // akcja niedozwolona
        // flyingBirds.add(penguin);

        for(Bird bird: birdList){
            bird.eat();
        }

        for(FlyingBird flyingBird: flyingBirds){
            flyingBird.fly();
        }
    }
} 

Teraz klasa Penguin nie łamie zasady LSP, ponieważ nie implementuje w żaden sposób metody fly(), której nie mogłaby wywołać w poprawny sposób.

Dzięki zastosowaniu zasady podstawienia Liskov kod stał się znacznie prostszy do rozszerzenia i testowania.