Liskov Substitution Principle (zasada podstawienia Liskov)

Zasada podstawienia Liskov (Liskov Substitution Principle) to  jedno z najważniejszych założeń dobrego programowania obiektowego. Jej głównym celem jest zapewnienie, aby klasy pochodne mogły bez problemu zastępować swoje klasy bazowe, nie zmieniając poprawności działania programu. W praktyce oznacza to, że jeśli korzystamy z klasy nadrzędnej, powinniśmy móc podstawić w jej miejsce dowolną klasę dziedziczącą – i nasz kod nadal będzie działał zgodnie z oczekiwaniami.

Brzmi abstrakcyjnie, ale LSP ma bardzo konkretne znaczenie dla jakości kodu: pomaga utrzymać spójność, unikać błędów logicznych i tworzyć systemy, które są bardziej przewidywalne oraz łatwiejsze w rozbudowie.

Teraz przedstawię przykład kodu, który łamie zasadę podstawienia Liskov:

Mamy taką klasę Bird:

public class Bird {
    public void fly() {
        System.out.println("The bird files!");
    }
}

Następnie – tworzymy klasę Penguin, która dziedziczy po klasie Bird

class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguin can't fly!");
    }
}

Przy wyoływaniu funkcji dla obiektu Penguin pojawi się błąd, który zdefiniowałam w powyższej klasie. Oczywiście pingwin nie potrafi latać, więc takie dziedziczenie jest bez sensu i psuje logikę programu.

Poprawiona wersja przykładu zgodna z zasadą podstawienia Liskov.

Zmieniamy klasę Bird na abstrakcyjną i definiujemy przykładową metodę wspólną dla różnych ptaków (tutaj eat()):

abstract class Bird {
    abstract void eat();
}

Następnie tworzę interfejs CanFly

interface CanFly {
    void fly();
}
class FlyingBird extends Bird {
    void fly() {
        System.out.println("The bird flies!");
    }
}
class Sparrow extends Bird implements CanFly{
    @Override
    void eat() {
        System.out.println("Sparrow eats grains and insects.");
    }

    @Override
    public void fly() {
        System.out.println("Sparrow can flies.");
    }
}
class Penguin extends Bird {
    @Override
    void eat(){
        System.out.println("Penguin eats fish.");
    }
}

public class Demo {
    public static void main(String[] args){
        Bird sparrow = new Sparrow();
        Bird penguin = new Penguin();

        sparrow.eat();
        penguin.eat();

        CanFly flyingSparrow = (CanFly) sparrow;
        flyingSparrow.fly();
    }
}

W tym przykładzie pokazałam, jak łatwo można złamać zasadę podstawienia Liskov, gdy klasa potomna nie spełnia kontraktu klasy bazowej. W pierwotnej wersji Penguin dziedziczył po Bird z metodą fly(), co prowadziło do błędów w czasie działania programu – obiekt pingwina nie mógł zastąpić ogólnego ptaka bez psucia logiki.

Rozwiązaniem było wydzielenie wspólnego kontraktu w klasie bazowej (Bird) i oddzielenie specyficznych zachowań, takich jak latanie, do osobnego interfejsu (CanFly). Dzięki temu każda klasa potomna:

  • spełnia kontrakt klasy bazowej (eat()),

  • zachowuje się przewidywalnie i bezpiecznie,

  • nie „wymusza” implementacji metod, które nie mają sensu dla danego typu.

Zasada Liskov Substitution Principle uczy nas projektować klasy w taki sposób, aby można je było bezpiecznie podmieniać w programie. Dzięki temu kod staje się bardziej elastyczny, łatwiejszy do rozbudowy i mniej podatny na błędy, a każda nowa klasa w hierarchii nie wprowadza nieprzewidzianych problemów.

Przewijanie do góry