Liskov Substitution Principle (LSP)

Terminologies:

  • base class means parent class

  • subclass / derived class means child class

↝ This principle means that derived classes must be substitutable for their base classes. What do we mean by that? It means that a subclass should be able to be used in the same manner as the parent class. Treat it as something akin to a contract; whatever works for the base class should always work fully in the subclass.

In other words, a subclass should work just like how you use the base class. That's why there's "substitution" in its name because the subclass can substitute the base class. If there's a method or property that does not work with the subclass, it can't fully substitute the base class, hence, you violate this principle. Let's take a look at the code below in TypeScript:

class Vehicle {
  refuel() {
    return "Vehicle is now refueling.";
  }

  startEngine() {
    // Logic to start the engine
  }

  stopEngine() {
    // Logic to stop the engine
  }
}

class HondaBRV extends Vehicle {
  // Some code ...
}

class TeslaModelY extends Vehicle {
  // Some code ...
}

As you've observed from the above code, it violates the LSP. Why? Both TeslaModelY and HondaBRV are vehicles, but TeslaModelY is an electric vehicle. Therefore, the refuel method will do something that TeslaModelY does not expect, because the refuel method will surely need gasoline, whereas the TeslaModelY needs electricity.

So, what can be improved here to follow the LSP? We will create a new base class where we put all the commonalities of all the vehicles. Let's see the TypeScript code below:

class Vehicle {
  startEngine(): void {
    console.log("Starting an engine");
  }
  stopEngine(): void {
    console.log("Stopping an engine");
  }
}

class HondaBRV extends Vehicle {
  refuel(): void {
    console.log("Refueling...");
  }
}

class TeslaModelY extends Vehicle {
  recharge(): void {
    console.log("Recharging...");
  }
}

So what we did here is we created a new class that will implement all the commonalities of the class. Then, each class will need to implement their own refuel and recharge method. In this case, whenever we use the Vehicle class, whatever properties or methods it may have, it will surely work on subclasses, meaning no unexpected behavior will happen in the future.

But, there's a problem. What if we need to accommodate other electric and gas vehicles? What should we do? See the example below:

class Vehicle {
  startEngine(): void {
    console.log("Starting an engine");
  }
  stopEngine(): void {
    console.log("Stopping an engine");
  }
}

class GasVehicle extends Vehicle {
  refuel(): void {
    console.log("Refueling...");
  }
}

class ElectricVehicle extends Vehicle {
  recharge(): void {
    console.log("Recharging...");
  }
}

class HondaBRV extends GasVehicle {
  // Other fields
}

class HondaCity extends GasVehicle {
  // Other fields
}

class TeslaModelY extends ElectricVehicle {
  // Other fields
}

class TeslaCyberTruck extends ElectricVehicle {
  // Other fields
}

What we did here is we created two new classes: ElectricVehicle and GasVehicle. Both will implement how cars are recharged or fueled, but the idea is that this will bridge the Vehicle class and other classes depending on their type—whether an electric car or a gas car.

To sum up, we've seen how helpful inheritance can be, but it can also introduce inconsistencies or issues if not handled well. However, with LSP, it helps us avoid those inconsistencies by considering a subclass as a specialized type of the superclass. So, it's still an instance of the superclass, with some additional behaviors. Again, your subclass should be a substitute for the base class; if the subclass can't use a method from the base class, then you have already violated the LSP. Thank you and happy coding!