Liskov substitution principle#
tl;dr Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
The Liskov Substitution Principle (LSP) is a fundamental principle in object-oriented programming that helps ensure the correct and reliable behavior of code when using an inheritance. In simple terms, it states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
Think of it in terms of real-world objects. Let’s say we have a superclass called “Animal” and two subclasses called “Dog” and “Cat.” According to the Liskov substitution principle, if we have a piece of code that interacts with an “Animal” object, we should be able to replace that object with either a “Dog” or a “Cat” object, and the code should continue to work as expected.
In order to satisfy the Liskov substitution principle, the subclasses must adhere to certain rules:
The behavior of the subclass methods should match or be more specific than the behavior defined in the superclass. This means that a subclass should not introduce unexpected or different behavior that could break the code that relies on the superclass.
The subclass methods should respect the preconditions and postconditions defined by the superclass methods. Preconditions are the requirements or assumptions that must be true before a method is called, and postconditions are the guarantees or results that the method promises to deliver. The subclass should meet these requirements and deliver the expected results, ensuring that the code remains reliable.
By following the Liskov substitution principle, we can design our code to be more flexible, extensible, and maintainable. It allows us to create hierarchies of related classes, where objects can be substituted interchangeably, promoting code reusability and ensuring that the behavior remains consistent and predictable throughout the inheritance hierarchy.
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int calculateArea() {
return width * height;
}
}
public class Square extends Rectangle {
public Square(int sideLength) {
super.setWidth(sideLength);
super.setHeight(sideLength);
}
}
public class LspViolationExample {
public static void main(String[] args) {
Rectangle rectangle = new Square(5);
rectangle.setWidth(4);
rectangle.setHeight(5);
int area = rectangle.calculateArea(); // This will return 25, not 20
System.out.println("Area: " + area);
}
}
In the above example, we have a Rectangle reference to a Square object. When we set the width and height independently, it doesn’t behave as expected for a Square. When we calculate the area, it produces an incorrect result, thus violating the LSP. Now, let’s correct this violation. We can make the Square class behave correctly as a subtype of Rectangle by overriding the setWidth() and setHeight() methods to update both dimensions of the Square simultaneously, maintaining the square proportions.
Now, let’s correct this violation. We can make the Square class behave correctly as a subtype of Rectangle by overriding the setWidth() and setHeight() methods to update both dimensions of the Square simultaneously, maintaining the square proportions:
class Rectangle {
protected int width;
protected int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int calculateArea() {
return width * height;
}
}
class Square extends Rectangle {
public Square(int sideLength) {
super(sideLength, sideLength);
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
class LspExample {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(5, 4);
int rectangleArea = rectangle.calculateArea();
System.out.println("Rectangle Area: " + rectangleArea);
Square square = new Square(5);
int squareArea = square.calculateArea();
System.out.println("Square Area: " + squareArea);
}
}
In this updated example, we modified the behavior of the Square class to ensure that it respects the properties and behavior of a Rectangle. We achieve this by overriding the setWidth() and setHeight() methods to update both dimensions of the Square simultaneously, maintaining the square proportions.
Now, when we calculate the area of a Square or a Rectangle, we get the correct results. The Square class is now a proper subtype of Rectangle and adheres to the Liskov Substitution Principle.
By fixing the behavior and ensuring that the subclass doesn’t introduce unexpected or inconsistent behavior, we establish a correct inheritance hierarchy that allows objects of the superclass (Rectangle) to be replaced by objects of the subclass (Square) without any issues, maintaining the correctness of the program.