Skip to content

Understanding Liskov Substitution Principle

The Liskov Substitution Principle is one of the five SOLID principles of object-oriented design. It was introduced by Barbara Liskov in 1987 and is named after her.

Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.

In other words, if S is a subtype of T, then objects of type T can be replaced with objects of type S without altering the desirable properties of the program. This means that the behavior of the subclass should be consistent with the behavior of the superclass.

Example with Rectangle and Square that violates LSP

In this example we will see that the Square class violates the Liskov Substitution Principle by changing the behavior of the Rectangle class.

Suppose we have a class Rectangle and a class Square that extends Rectangle. The Rectangle class has two properties width and height, and a method getArea that calculates the area of the rectangle.

The Square class overrides the setHeight and setWidth methods to ensure that the width and height are always equal.

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

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  setWidth(width: number) {
    this.width = width;
  }

  setHeight(height: number) {
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width: number) {
    this.width = width;
    this.height = width; // Force height to be the same as width
  }

  setHeight(height: number) {
    this.width = height; // Force width to be the same as height
    this.height = height; 
  }
}

const myRectangle = new Rectangle(10, 5);
const mySquare = new Square(5);

// Violates LSP
// Changes both the width and height to 10. 
// This is not the behavior expected from a Rectangle object, where changing the width should not affect the height.
mySquare.setWidth(10);
console.log(mySquare.getArea());

In this case, the Square class breaks the expected behavior of the Rectangle class because it changes the logic of setting the width and height. The Rectangle class allows different widths and heights, but the Square forces both to be the same. Substituting Square in place of Rectangle causes unexpected results, thus violating Liskov Substitution.

How to avoid violation of LSP

To follow LSP, we can refactor the code so that Square and Rectangle do not inherit from each other but instead share a common interface or abstract class. This way, each shape can manage its dimensions according to its own logic without violating expectations.

// Define a Shape interface
interface Shape {
  getArea(): number;
}

// Rectangle implements Shape
class Rectangle implements Shape {
  protected width: number;
  protected height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  setWidth(width: number) {
    this.width = width;
  }

  setHeight(height: number) {
    this.height = height;
  }

  getArea(): number {
    return this.width * this.height;
  }
}

// Square implements Shape, but not Rectangle
class Square implements Shape {
  private side: number;

  constructor(side: number) {
    this.side = side;
  }

  setSide(side: number) {
    this.side = side;
  }

  getArea(): number {
    return this.side * this.side;
  }
}

// Usage
const myRectangle = new Rectangle(10, 5);
console.log(myRectangle.getArea()); // Outputs 50

const mySquare = new Square(5);
mySquare.setSide(10);
console.log(mySquare.getArea()); // Outputs 100

Conclusion

The Liskov Substitution Principle is an important concept in object-oriented design that helps us to write better software by ensuring that objects of a superclass can be replaced with objects of its subclasses without affecting the correctness of the program. By following this principle, we can create more maintainable, flexible, and reliable code that is easier to understand and work with.