Liskov Substitution Principle

A Subclass Must Be a Replaceable Substitute for Its Base Class

The Liskov Substitution Principle (LSP) states that objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program.

Simply put:

If class B inherits from class A, then anywhere we use A, we should be able to use B and the program should continue to work correctly.

This principle is not only about method signatures but, more importantly, about behavior. A derived class must not change the expected semantics of methods inherited from the base class.

Real-Life Example: How to Understand It?

Imagine a car rental company.

A customer walks in and says:

“I’d like to rent a car.”

The rental company hands over the keys and points to a vehicle in the parking lot.

The customer assumes that the car will:

  • start when the key is turned,
  • stop when the brake pedal is pressed,
  • move when the accelerator is pressed.

Now imagine the rental company gives the customer a vehicle with the engine removed.

It still looks like a car. It has a key, a steering wheel, and pedals. However, when the key is turned, nothing happens.

Is it still a “car” according to the agreement with the customer?

Technically, yes.

Practically, absolutely not.

The customer has every right to expect that every vehicle provided by the rental company behaves like a car. The rental company violated that expectation by supplying an object that looks like a car but does not behave like one.

Programming works exactly the same way.

If we have a Car class with a start() method and create a subclass CarWithoutEngine that overrides the method and throws an exception, we violate LSP.

Any piece of code that expects a Car receives an object that does not fulfill the contract.

A Thought Experiment

Consider the classic programming example of a rectangle and a square.

At first glance, a square is a rectangle (every square is a rectangle).

But should a Square class inherit from a Rectangle class in code?

Let’s see where things break down.

Example — LSP Violation

Suppose we are building a system for calculating the areas of geometric shapes.

We start with a rectangle:

// Base class: Rectangle with independent dimensions
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;
    }
}

Mathematically, a square is a special type of rectangle, so we inherit from it:

/ Square inherits from Rectangle – seems logical at first
class Square extends Rectangle {
 
    // A square must have equal sides,
    // so we override the setters
    @Override
    public void setWidth(int width) {
        // Set BOTH dimensions to preserve square properties
        this.width = width;
        this.height = width; // ← problem starts here
    }
 
    @Override
    public void setHeight(int height) {
        // Both dimensions must remain equal
        this.width = height; // ← silently changes another field
        this.height = height;
    }
}

Now let’s write a method that operates on a Rectangle and see what happens when we pass a Square.

// According to LSP, a method expecting Rectangle
// should also work with Square
public static void testArea(Rectangle r) {
    r.setWidth(5);
    r.setHeight(3);
 
    // Expected: 5 * 3 = 15
    int expected = 15;
    int actual = r.calculateArea();
 
    System.out.println(“Expected: ” + expected);
    System.out.println(“Actual: ” + actual);
 
    // Rectangle: prints 15 
    // Square:    prints 9  
}

Following:

public static void main(String[] args) {
    testArea(new Rectangle()); // works correctly
    testArea(new Square());    // incorrect result, LSP violated
}
What’s Wrong with This Approach?

The derived class changes the behavior of base-class methods.

In the Rectangle class, setWidth() changes only the width.

In the Square class, the same method also changes the height, without the caller’s knowledge.

Code that trusted the contract of the base class no longer works correctly.

The testArea() method does not know that it received a Square, and it should not need to know.

That is the essence of an LSP violation.

The inheritance hierarchy reflects a mathematical relationship, not a programming relationship.

In geometry, a square is a rectangle.

In object-oriented programming, that is not necessarily true if their behavior differs.

Example — LSP-Compliant Design

Instead of forcing a square into a rectangle hierarchy, we extract a shared abstraction representing what both shapes truly have in common: the ability to calculate an area.

/ Common abstraction
abstract class Shape {
    public abstract int calculateArea();
}

Then:



// Rectangle - independent dimensions

class Rectangle extends Shape {




    private int width;

    private int height;




    public Rectangle(int width, int height) {

        this.width = width;

        this.height = height;

    }




    @Override

    public int calculateArea() {

        return width * height;

    }

}

Add a class type:

/ Square – one side, its own logic
class Square extends Shape {
 
    private int side;
 
    public Square(int side) {
        this.side = side;
    }
 
    @Override
    public int calculateArea() {
        return side * side;
    }
}

Now a method operating on Shape works correctly for both classes:

public static void printArea(Shape shape) {
    System.out.println(“Shape area: ” + shape.calculateArea());
}
public static void main(String[] args) {
    printArea(new Rectangle(5, 3)); // Shape area: 15 
    printArea(new Square(4));       // Shape area: 16 
}

What Did We Gain?

Each class is responsible for its own logic.

The Square class no longer pretends to be a rectangle. It has its own consistent behavior.

The base-class contract is always honored.

calculateArea() in every subclass does exactly what the caller expects: it returns the area of the shape.

We can safely substitute subclasses.

Any code that accepts a Shape will work correctly whether it receives a Rectangle, a Square, or any other future shape.

Why Is LSP Important?

Inheritance Is a Promise

When we write:

class B extends A

we are making a promise:

“B behaves like A wherever A is expected.”

Violating LSP breaks that promise and introduces bugs that are often difficult to debug because the code still compiles successfully.

Predictable Behavior

When LSP is respected, we can trust that subclasses will not introduce surprises.

A method that works for the base class will also work for every subclass without requiring special handling.

Eliminating instanceof Checks

The need to write code such as:

if (object instanceof Square)

is often a sign of an LSP violation.

The code must distinguish between types because subclasses are not truly substitutable for their base class.

Better Team Collaboration

In large projects, LSP violations often surface months later.

Someone adds a new subclass and suddenly part of the system behaves differently.

Respecting LSP gives the team confidence that extending class hierarchies is safe.

Consequences in Large Systems

Imagine a payment system with a base class Payment and dozens of subclasses representing different payment methods.

If one subclass throws:

UnsupportedOperationException

for a method such as:

refund()

the refund mechanism breaks for that payment type.

Customers do not receive their money back, and the bug may only be discovered in production.

When Should You Apply LSP?

Whenever You Use Inheritance

Before creating a subclass, ask yourself:

Can this subclass replace the base class in every scenario?

If the answer is:

“Not always”

then the inheritance hierarchy is likely incorrect.

When instanceof Appears in the Code

Type checking is often a sign that subclasses are not true substitutes for the base class.

public void processShape(Shape shape) {
 
    if (shape instanceof Square) {
        // special handling
    } else {
        // standard handling
    }
}
In E-Commerce Systems

Consider an Order class and an ExportOrder subclass.

If ExportOrder does not support calculateDiscount() in the same way as a normal order, it violates LSP and can cause failures in the discount system.

In Spring Framework

Spring heavily relies on LSP.

Any Bean can be replaced by a mock implementation during testing.

That is one of the reasons dependency injection through interfaces is so powerful: every implementation acts as a valid substitute.


“If it looks like a duck and quacks like a duck—but requires batteries to quack—you have a bad inheritance model.”

A subclass must not only look like the base class—it must also behave like it.

No surprises.

No exceptions.

No silent behavioral changes.

Inheritance is not just about sharing code—it is a commitment to honoring a contract.

A derived class may extend the behavior of a base class, but it should never break that behavior.

CATEGORIES:

SOLID Principles

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *

Latest Comments

No comments to show.