Trzecia z zasad SOLID to Liskov Substitution Principle – zasada podstawienia Liskov. Zasada ta została sformułowana przez amerykańską programistkę – Barbarę Liskov. Jej zasadnicza treść brzmi tak: „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.” Inaczej mówiąc, w miejsce typu bazowego możemy podstawić dowolny typ klasy pochodnej i nie powinniśmy utracić poprawnego działania. 

Dla zobrazowania złamania tej zasady przywołajmy książkowy przykład z klasą Rectangle oraz klasą Square, która dziedziczy po Rectangle, w myśl matematycznej zasady, że każdy kwadrat jest prostokątem.

Rectangle.java

package calculator.area;

public class Rectangle {
    protected double width;
    protected double height;

    public void setWidth(double width) {
        this.width = width;
    }

    public void setHeight(double height) {
        this.height = height;
    }
    
    public double calculateArea() {
        return width * height;
    }
}

Square.java

package calculator.area;

public class Square extends Rectangle{

    public void setWidth(double width){
        this.width = width;
        this.height = height;
    }

    public void setHeight(double height) {
        this.height = height;
        this.width = height;
    }
}

Main.java

package calculator;


import calculator.area.Rectangle;
import calculator.area.Square;

public class Main {

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(3);
        rectangle.setHeight(4);
        System.out.println("Area: " + rectangle.calculateArea());

        Rectangle rectangleSquare = new Square();
        rectangleSquare.setWidth(3);
        rectangleSquare.setHeight(4);
        System.out.println("Area: " + rectangleSquare.calculateArea()); //16

        Square square = new Square();
        square.setWidth(3);
        square.setHeight(4);
        System.out.println("Area: " + square.calculateArea()); //16
    }

}

W programowaniu wspomniana powyżej matematyczna zasada wprowadza w błąd. W czasie ustawiania wartości dla drugiego boku, ponownie przestawiana jest wartość pierwszego boku. W ten sposób do obliczeń powierzchni brana jest pod uwagę ostatnia ustawiona wartość – w tym przypadku 4. W naszym przykładzie dla klasy Square dobrze byłoby ustalić jedną zmienną oznaczającą długość boku – po prostu side. Tak więc nie potrzeba, aby klasa Square dziedziczyła po Rectangle, wystarczy stworzyć klasę abstrakcyjną Shape z polem area i metodą calculateArea, po której to klasie dziedziczyć będą Reactangle i Square. Poniższy przykład prezentuje poprawne zaimplementowanie zasady LSP.

Shape.java

package calculator.area;

public abstract class Shape {
    
    protected double area;
    public abstract double calculateArea();
}

Rectangle.java

package calculator.area;

public class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    @Override
    public double calculateArea() {
        area =  width * height;
        return area;
    }
}

Square.java

package calculator.area;

public class Square extends Shape{

    private double side;

    public Square(double side) {
        this.side = side;
    }

    public double getSide() {
        return side;
    }

    public void setSide(double side) {
        this.side = side;
    }

    @Override
    public double calculateArea() {
        area = side * side;
        return area;
    }
}

Main.java

package calculator;


import calculator.area.Rectangle;
import calculator.area.Square;

public class Main {

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(3, 4);
        System.out.println("Area: " + rectangle.calculateArea());

        Square square = new Square(3);
        System.out.println("Area: " + square.calculateArea());
    }

}

PODSUMOWANIE

Tak więc zasada podstawienia Liskov mówi o tym, że klasy dziedziczące powinny w odpowiedni sposób implementować metody z klas bazowych. Zasada ta łączy się z zasadą „otwarty-zamknięty”, ponieważ dzięki jej zastosowaniu mamy możliwość rozszerzania istniejącego kodu bez możliwości modyfikacji już istniejącego kodu, istniejących klas. Zasada LSP dotyczy więc prawidłowo zaprojektowanego dziedziczenia, w którym powinniśmy mieć możliwość zastąpienia klasy bazowej klasami pochodnymi. Jeśli tak nie jest, to oznacza to źle zaimplementowane dziedziczenie. Warto tutaj wspomnieć także, że klasa pochodna nie może mieć silniejszych warunków wstępnych niż klasa bazowa oraz warunki podrzędne nie mogą być słabsze w klasach pochodnych, w stosunku do klasy bazowej.