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.”
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.