Liskov Substitution Principle (LSP) – SOLID Principles in Java

Liskov Substitution Principle (LSP) – SOLID Principles in Java

Introduction

The Liskov Substitution Principle (LSP) is the third principle in the SOLID design principles for object-oriented programming. It was introduced by Barbara Liskov in 1987 and states:

Objects of a derived (sub) class must be substitutable for objects of the base (parent) class without affecting the correctness of the program.

Breaking It Down in Simple Terms

Imagine you have a system that expects a parent class (Rectangle). If you replace it with a child class (Square), everything should work without errors or unexpected results. If using a subclass breaks the logic or changes the expected behavior, then LSP is violated.

If a class inherits from another, but it modifies the behavior in unexpected ways, it should not be a subclass – it should be a separate entity!

Think of a Bird class with a fly() method. If we create a Penguin subclass but override fly() to throw an error (because penguins cannot fly), then LSP is violated. The solution? Create a FlyingBird interface instead of forcing Penguin to inherit unwanted behavior.


Why is LSP Important?

Without LSP, subclasses introduce unexpected behavior, which leads to:

? Code that breaks when subclasses replace base classes – making inheritance useless.

? Unexpected results and hidden bugs – because the subclass does not behave like its parent.

? Violations of Open/Closed Principle (OCP) – since modifications in the subclass require changes in client code.

? Difficult debugging and maintenance – because behavior is unpredictable.

The solution? Ensure that subclasses truly behave as their base class and do not introduce conflicting logic.


?? Bad Example – Without LSP

Let’s take an example of Rectangle and Square. A square is a rectangle where width and height are always the same, but making Square a subclass of Rectangle causes issues.

Problem: Inheritance Gone Wrong

  • Square forces width and height to be equal, overriding the expected behavior of Rectangle.
  • If a client expects a Rectangle, passing a Square breaks the calculations.

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 getArea() {
        return width * height;
    }
}        
class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width); // Forces width and height to be equal
    }

    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}        

Why is this bad?

? Breaks LSP: Square does not behave like a Rectangle because setting width changes height and vice versa.

? Unexpected Behavior: When the program expects a Rectangle, using Square causes incorrect calculations.

? Violates Polymorphism: The class structure cannot be reliably extended.

Failing Scenario: Unexpected Output

public class LSPViolation {
    public static void main(String[] args) {
        Rectangle rectangle = new Square();
        rectangle.setWidth(5);
        rectangle.setHeight(10);
        
        System.out.println("Expected Area: 5 x 10 = 50");
        System.out.println("Actual Area: " + rectangle.getArea()); 
        // Prints 100 instead of 50!
    }
}        

What’s Wrong Here?

  • The system expects a Rectangle, where width and height can be set separately.
  • But since Square forces both to be equal, setting height also changes the width.
  • The expected area calculation is broken – violating LSP.


?? Good Example – Applying LSP Correctly

The issue is that Square should not inherit Rectangle because its behavior is fundamentally different. Instead, we should use composition instead of inheritance.

Solution: Creating an Abstract Interface

  • Create a Shape interface that defines common behavior.
  • Implement Rectangle and Square independently while keeping their expected behavior intact.

interface Shape {
    int getArea();
}        
class Rectangle implements Shape {
    protected int width;
    protected int height;

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

    @Override
    public int getArea() {
        return width * height;
    }
}        
class Square implements Shape {
    private int side;

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

    @Override
    public int getArea() {
        return side * side;
    }
}        

Refactored Client Code (Now LSP Compliant!)

public class LSPExample {
    public static void main(String[] args) {
        Shape rectangle = new Rectangle(5, 10);
        Shape square = new Square(5);
        
        System.out.println("Rectangle Area: " + rectangle.getArea());
        System.out.println("Square Area: " + square.getArea());
    }
}        

Why is this better?

? LSP Compliant: Rectangle and Square can be used interchangeably through Shape.

? No Unexpected Behavior: Square behaves as expected.

? Extensible & Maintainable: We can add new shapes without modifying existing classes.


?? Key Takeaways

?? LSP ensures subclasses behave correctly when replacing base classes.

?? Use composition over inheritance when behaviors differ significantly.

?? Violating LSP leads to runtime errors and broken implementations.

?? If a subclass requires modifying inherited behavior, it likely should not be a subclass.


The Liskov Substitution Principle (LSP) ensures that subtypes can replace base types without breaking the program. By designing correct inheritance structures and using interfaces/composition instead of forcing inheritance, we create scalable and maintainable software.

Next up: Interface Segregation Principle (ISP) – "Small, Specific Interfaces Are Better" Stay tuned!

要查看或添加评论,请登录

Kalana De Silva的更多文章