Back to SOLID Principles

Definition

The Liskov Substitution Principle (LSP) is the third of the five SOLID principles of object-oriented design. It states that:

"Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."

In other words, if class B is a subtype of class A, then we should be able to replace objects of type A with objects of type B without disrupting the behavior of our program.

Bad Example: Violating LSP
// Base class
class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }
    
    setWidth(width) {
        this.width = width;
    }
    
    setHeight(height) {
        this.height = height;
    }
    
    getArea() {
        return this.width * this.height;
    }
}

// Derived class that violates LSP
class Square extends Rectangle {
    constructor(size) {
        super(size, size);
    }
    
    // This violates LSP because it changes the behavior
    setWidth(width) {
        this.width = width;
        this.height = width; // Square must maintain equal sides
    }
    
    // This also violates LSP
    setHeight(height) {
        this.height = height;
        this.width = height; // Square must maintain equal sides
    }
}
Why This Violates LSP:

This implementation violates the Liskov Substitution Principle for several reasons:

  • The Square class changes the behavior of the setWidth and setHeight methods.
  • Code that works with a Rectangle expects to be able to change width and height independently.
  • If we substitute a Square for a Rectangle, the program may behave incorrectly.
  • Consider this function that would work with a Rectangle but fail with a Square:
function increaseRectangleWidth(rectangle) {
    const initialHeight = rectangle.height;
    rectangle.setWidth(rectangle.width + 10);
    // With a Square, this assertion would fail
    assert(rectangle.height === initialHeight);
}
Good Example: Following LSP
// Base shape interface/class
class Shape {
    getArea() {
        throw new Error("Method 'getArea()' must be implemented.");
    }
}

// Rectangle implementation
class Rectangle extends Shape {
    constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
    }
    
    setWidth(width) {
        this.width = width;
    }
    
    setHeight(height) {
        this.height = height;
    }
    
    getArea() {
        return this.width * this.height;
    }
}

// Square as a separate implementation, not derived from Rectangle
class Square extends Shape {
    constructor(size) {
        super();
        this.size = size;
    }
    
    setSize(size) {
        this.size = size;
    }
    
    getArea() {
        return this.size * this.size;
    }
}
Why This Follows LSP:

This implementation follows the Liskov Substitution Principle:

  • Both Rectangle and Square extend the Shape base class.
  • Each class has its own appropriate interface that doesn't violate expectations.
  • Square doesn't try to override Rectangle methods in a way that changes behavior.
  • Code that expects a Shape will work correctly with either a Rectangle or a Square.
  • The hierarchy accurately reflects the relationship between the shapes.

This approach recognizes that a square is not a special type of rectangle in terms of behavior in an object-oriented system, even though mathematically it is.

Interactive Demo: Rectangle vs Square

This demo shows how the Liskov Substitution Principle works with rectangles and squares. Try changing the dimensions and see how the different implementations behave.

Results:

Click a button to run a test.
Back to Main Page