Lesson 3 of 5

Liskov Substitution Principle

If someone hands you "a phone charger," you expect it to charge your phone — no matter which brand made it. If one particular charger secretly only "charges" phones that are already at 100%, it's lying about being a phone charger. Named after computer scientist Barbara Liskov, the Liskov Substitution Principle (LSP) says: a subclass must be usable anywhere its parent class is expected, without breaking the caller's expectations.

💡 The Bottom Line

If code works with a base class, it should keep working — unmodified — when you hand it any subclass instead. If using a subclass means the caller has to add special-case checks just to avoid it blowing up, that subclass has broken the contract of its parent.

The classic example: Rectangle and Square

Mathematically, a square is a rectangle with equal sides — so it seems reasonable to make Square extends Rectangle. Here's where it goes wrong:

class Rectangle {
  setWidth(w)  { this.width = w; }
  setHeight(h) { this.height = h; }
  area() { return this.width * this.height; }
}

class Square extends Rectangle {
  setWidth(w)  { this.width = w; this.height = w; }  // must keep both sides equal
  setHeight(h) { this.width = h; this.height = h; }
}

function resizeAndCheck(rect) {
  rect.setWidth(5);
  rect.setHeight(10);
  console.assert(rect.area() === 50, "Expected area 50");
}

resizeAndCheck(new Rectangle()); // passes: area is 50
resizeAndCheck(new Square());    // fails: area is 100 — setHeight silently changed width too

Any code written and tested against Rectangle silently breaks the moment a Square is passed in instead. Square is a valid shape mathematically, but it's not a valid substitute for Rectangle in code — it violates the caller's assumption that width and height can be set independently.

⚠️ Watch Out

LSP violations are sneaky because the code still compiles and the inheritance still makes conceptual sense in plain English ("a square is a rectangle"). The break only shows up at runtime, often in a caller far away from where the subclass was defined — which is exactly why it's a "principle" to watch for, not something a type checker alone will catch.

Signs you're violating LSP

🙋 There Are No Dumb Questions

Q: So should Square never inherit from Rectangle?
A: Not through mutable setWidth/setHeight methods, no — that's exactly the shape that breaks. If both were immutable (a Rectangle that only exposes a read-only area(), built once at construction), a Square could satisfy that same contract without surprises. The lesson isn't "never model Square as a Rectangle" — it's "design the parent's contract so every subclass can honestly keep it."

The fix: design the contract subclasses can actually keep

Instead of forcing a hierarchy where it doesn't fit, model what's actually shared:

class Shape {
  area() { throw new Error("must implement area()"); }
}

class Rectangle extends Shape {
  constructor(width, height) { super(); this.width = width; this.height = height; }
  area() { return this.width * this.height; }
}

class Square extends Shape {
  constructor(side) { super(); this.side = side; }
  area() { return this.side * this.side; }
}

Both are honest Shapes with an area() method that behaves exactly as promised — neither one secretly mutates state the other didn't ask for.

🔧 Try It Yourself

A Bird base class has a fly() method. A Penguin subclass overrides fly() to throw an error, since penguins can't fly. Explain why this violates LSP, and sketch an alternative class design (hint: what if fly() weren't on the base Bird class at all?).

✅ Quick Check

A function works correctly with every subclass of PaymentMethod — except one subclass throws an unexpected error for an operation the parent class always supported. What principle is being violated?


Next up: what happens when an interface forces classes to implement methods they don't actually need? That's the Interface Segregation Principle.

← Back to
Next →