Low-level design2 min

Liskov Substitution Principle

The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application.

In simpler terms: A child class must be perfectly substitutable for its parent class. If a subclass overrides a parent's method in a way that breaks the expectations of the client code, it violates LSP.

Real-Life Analogy

Think of a Coffee Machine. The instruction manual says "Insert a Coffee Pod to brew coffee".

  • If you insert an Espresso Pod (Subclass), it brews espresso. (Works fine).
  • If you insert a Decaf Pod (Subclass), it brews decaf. (Works fine).
  • If you insert a Plastic Toy Pod (Bad Subclass), the machine jams and breaks. The Toy Pod violated the expectation of what a "Coffee Pod" should do.

Example: Violation of LSP

The classic example of LSP violation is the Rectangle-Square problem.

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

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

// A Square IS-A Rectangle, right? Mathematically yes, but behaviorally no!
class Square extends Rectangle {
  // Overriding to force width and height to be identical
  public setWidth(width: number): void {
    this.width = width;
    this.height = width; // Changing height unexpectedly!
  }

  public setHeight(height: number): void {
    this.height = height;
    this.width = height; // Changing width unexpectedly!
  }
}

// Client Code expects a Rectangle
function makeItWider(rect: Rectangle): void {
  rect.setWidth(10);
  rect.setHeight(5);
  // Expectation: Area should be 50.
  console.log(rect.getArea()); 
}

const myRect = new Rectangle();
makeItWider(myRect); // Prints 50. Works perfectly.

const mySquare = new Square();
makeItWider(mySquare); // Prints 25! LSP is violated! 
// The Square behaved unexpectedly when treated as a Rectangle.

Example: Proper LSP Design

If a Square cannot behave like a Rectangle, it should not inherit from it. Instead, they should both implement a generic Shape interface.

typescript
interface Shape {
  getArea(): number;
}

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

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

Why is it Important?

  • Reliability: Ensures that polymorphism works correctly. When you use an interface or base class, you can trust that any subclass passed into your function will behave properly without needing to add messy if (obj instanceOf Square) checks.