- natalia.kaczynska.programista@gmail.com
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.