skip to content
~/amin
← cd ~/courses/solid

lesson 3/5 · 0:56

Liskov Substitution Principle

Why adding one subclass can crash code you never changed, and how to put the limit into the contract instead.

“A subtype must work anywhere its parent type works.”

— Barbara Liskov (paraphrased)

The story

Tony’s app gives every vehicle a full order. He adds a bike, and it tips over.

The problem in code

Dispatch loads any order into any Vehicle. Bike only carries two pizzas, so its load throws. Dispatch never changed, and now it crashes on bikes.

class Vehicle {
  load(order: Order) { /* carries anything */ }
}

class Bike extends Vehicle {
  load(order: Order) {
    if (order.pizzas > 2) throw new Error('Too many pizzas');
  }
}

function dispatch(order: Order, vehicle: Vehicle) {
  vehicle.load(order); // fine for vans, crashes for bikes
}

The fix

In code, “is a” means “behaves like”. Put the limit into the contract: every courier says what it can carry, and dispatch picks one that fits.

interface Courier {
  capacity: number;
  load(order: Order): void; // never throws for orders within capacity
}

function dispatch(order: Order, couriers: Courier[]) {
  const courier = couriers.find((c) => c.capacity >= order.pizzas);
  if (!courier) return queue(order);
  courier.load(order);
}

Bikes, vans and drones all plug in, and dispatch never needs a special case.

Takeaway

If a subclass throws, ignores, or narrows something its parent promised, callers break. Make the difference part of the shared contract instead of hiding it in an override.