Liskov Substitution Principle (LSP)

⭐ Interview Importance: MEDIUM
⏱️ Revision Time: 5 min

Concept

Introduced by Barbara Liskov, the Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

In simple terms: If it looks like a duck, quacks like a duck, but needs batteries, you probably have the wrong abstraction. A child class must fully honor the contracts and expectations set by its parent class. It cannot unexpectedly throw exceptions or change the fundamental rules of the parent.

The Problem (Violation)

The classic example is the Square-Rectangle problem. In mathematics, a Square is a Rectangle. But in code, modeling it via inheritance breaks LSP.

class Rectangle {
    protected width: number = 0;
    protected height: number = 0;

    setWidth(w: number) { this.width = w; }
    setHeight(h: number) { this.height = h; }
    getArea() { return this.width * this.height; }
}

class Square extends Rectangle {
    // A square must have equal sides, so we override the behavior
    setWidth(w: number) {
        this.width = w;
        this.height = w;
    }
    setHeight(h: number) {
        this.width = h;
        this.height = h;
    }
}

Now, let’s write a function that expects the base class (Rectangle):

function resizeAndCalculateArea(rect: Rectangle) {
    rect.setWidth(5);
    rect.setHeight(10);
    // We expect the area to be 50.
    return rect.getArea(); 
}

const mySquare = new Square();
console.log(resizeAndCalculateArea(mySquare)); 
// Prints 100! The Square altered the expected behavior of the Rectangle.

Because Square completely hijacked the behavior expected of a Rectangle (independent width and height), it is not substitutable. This violates LSP.

The Solution

Do not force inheritance where behaviors diverge. Instead, rely on a broader abstraction or separate the concepts.

interface Shape {
    getArea(): number;
}

class Rectangle implements Shape {
    constructor(private width: number, private height: number) {}
    getArea() { return this.width * this.height; }
}

class Square implements Shape {
    constructor(private side: number) {}
    getArea() { return this.side * this.side; }
}

Now, both classes honor the Shape interface contract without breaking unexpected mathematical assumptions.

Interview Questions

Q: What are common signs that LSP is being violated?
A:

  1. Subclasses throwing NotImplementedException for methods they inherited but don’t support.
  2. Code that does explicit type checking (e.g., if (obj instanceof SpecificSubclass)) to handle special cases.
  3. Subclasses returning completely different types or applying stricter validation rules than the parent class promised.