Liskov Substitution Principle

The Liskov Substitution Principle (LSP) says that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

In other words, a subclass should be able to substitute its superclass without breaking the functionality expected from the superclass.

LSP violation example

Let’s consider an interface Vehicle with methods startEngine() and move().

We’ll create two classes, Car and Bicycle, that implement this interface.

Car is fine because it correctly implements both methods, but Bicycle violates LSP because it doesn’t implement startEngine() properly.

interface Vehicle {
  startEngine(): void;
  move(): void;
}

class Car implements Vehicle {
  startEngine() {
    console.log('Engine started.');
  }

  move() {
    console.log('Car is moving...');
  }
}

class Bicycle implements Vehicle {
  startEngine() {
    throw new Error("Bicycles don't have engines!");
  }

  move() {
    console.log('Bicycle is moving...');
  }
}

Now, let’s add a function operateVehicle() that attempts to start the engine before moving the vehicle.

function operateVehicle(vehicle: Vehicle) {
  vehicle.startEngine();
  vehicle.move();
}

const car = new Car();
operateVehicle(car); // Engine started. Car is moving...

const bicycle = new Bicycle();
operateVehicle(bicycle); // throws an error

In the operateVehicle() function, we expect all Vehicle objects to have a functioning startEngine() method. However, when we try to use Bicycle, it throws an error because it doesn’t have an engine. This violates the LSP because we cannot substitute Bicycle for Vehicle without causing an issue.

How to fix it

To follow the LSP, we can change the design to avoid using a method that doesn’t apply to all subclasses. One approach is to create another interface for engine-related functionality.

interface Vehicle {
  move(): void;
}

interface EngineVehicle extends Vehicle {
  startEngine(): void;
}

class Car implements EngineVehicle {
  startEngine() {
    console.log('Engine started.');
  }

  move() {
    console.log('Car is moving...');
  }
}

class Bicycle implements Vehicle {
  move() {
    console.log('Bicycle is moving...');
  }
}

function operateVehicle(vehicle: EngineVehicle) {
  vehicle.startEngine();
  vehicle.move();
}

const car = new Car();
operateVehicle(car); // Engine started. Car is moving...

const bicycle = new Bicycle();
// operateVehicle(bicycle);
// ❌ This will not compile because Bicycle doesn't implement EngineVehicle

By separating the engine functionality into an EngineVehicle interface, we ensure that only vehicles that actually have engines implement the startEngine method. This allows us to follow LSP, making our code cleaner and more maintainable, while preventing errors when substituting incompatible classes.


Find this post helpful? Subscribe and get notified when I post something new!