Liskov substitution principle

Teoria

Definicja zasady podstawienia Liskov mówi: Funkcje, które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości tych obiektów. Tłumacząc to prostszy język, możemy powiedzieć, że w miejsce klasy bazowej powinniśmy mieć możliwość podstawienia dowolnej klasy dziedziczącej bez utraty poprawnego działania programu.

Z zasady tej wywodzą się dwie kolejne – warunki wstępne nie powinny być wzmacniane w typach pochodnych, a warunki końcowe nie powinny być w nich mniej restrykcyjne( należy zachować odpowiedni kierunek ich stopniowania).

Przykład

Rozważmy omawianą zasadę na podstawie popularnego przykładu z kaczkami. Poniższy kodu reprezentuje klasę kaczki, a w niej trzy metody odpowiedzialne za latanie/pływanie i chodzenie. Mamy również przedstawicieli tego gatunku w postaci kaczki krzyżówki oraz kaczki mandarynki, które po wspomnianej klasie dziedziczą.

public class Duck {

    public void fly(){
        System.out.println("I'm flying");
    }

    public void walk(){
        System.out.println("I'm walking");
    }

    public void swim(){
        System.out.println("I'm swimming");
    }
}




public class MallardDuck extends Duck{

    @Override
    public void fly() {
        System.out.println("I'm flying mallard duck");
    }

    @Override
    public void walk() {
        System.out.println("I'm walking mallard duck");;
    }

    @Override
    public void swim() {
        System.out.println("I'm swimming mallard duck");;
    }
}




public class MandarinDuck extends Duck{

    @Override
    public void fly() {
        System.out.println("I'm flying mandarin duck");
    }

    @Override
    public void walk() {
        System.out.println("I'm walking mandarin duck");;
    }

    @Override
    public void swim() {
        System.out.println("I'm swimming mandarin duck");;
    }
}

Taka konstrukcja kodu spełnia reguły podstawienia Liskov . Możemy stworzyć instancję (obiekt) klasy MandarinDuck lub MallardDuck i wywoływać na nich metody bez podawania konkretnego podtypu, nie ma możliwości, aby program został przez nas uszkodzony. Zastanówmy się jednak, co w momencie, jeśli chcielibyśmy dodać do grona klas dziedziczących po Duck kaczkę gumową? To nadal jest kaczka, jednak nie umie ona latać ani chodzić.

public class RubberDuck extends Duck{

    @Override
    public void fly() {
        //???
    }

    @Override
    public void walk() {
        //???
    }

    @Override
    public void swim() {
        System.out.println("I'm swimming rubber duck");
    }
}

Po wprowadzeniu odpowiednich zmian umożliwiających implementację gumowej kaczki, kod przestaje spełniać omawianą w artykule zasadę (dodatkowo łamie SRP i OCP). Rozwiązaniem tego problemu byłoby wprowadzenie dodatkowych klas jak SwimmingDuck oraz FlyingDuck i odpowiednie zrestrukturyzowanie dziedziczenia. Innym rozwiązaniem jest usunięcie bazowej klasy Duck i dodanie do programu interfejsów odpowiedzialnych za poszczególne czynności, a następnie „składanie” kaczek z odpowiednich metod ( ten sposób przedstawiony poniżej).

public interface Flying {
    void fly();
}


public interface Walking {

    void walk();
}


public interface Swimming {

    void swim();
}



public class MallardDuck implements Flying, Walking, Swimming{

    @Override
    public void fly() {
        System.out.println("I'm flying mallard duck");
    }

    @Override
    public void walk() {
        System.out.println("I'm walking mallard duck");;
    }

    @Override
    public void swim() {
        System.out.println("I'm swimming mallard duck");;
    }
}




public class MandarinDuck implements Flying, Walking, Swimming{

    @Override
    public void fly() {
        System.out.println("I'm flying mandarin duck");
    }

    @Override
    public void walk() {
        System.out.println("I'm walking mandarin duck");;
    }

    @Override
    public void swim() {
        System.out.println("I'm swimming mandarin duck");;
    }
}




public class RubberDuck implements Swimming{

    @Override
    public void swim() {
        System.out.println("I'm swimming rubber duck");
    }
}

Warto też mieć na uwadze, że słabym rozwiązaniem jest również pozostawienie problematycznej metody pustej, czy wyrzucenie błędu, bo to również narusza zasadę podstawienia Liskov.

Dodaj komentarz