Liskov Substitution Principle (LSP)


Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program..
In simpler terms, if class B is a subclass of class A, then objects of class A should be able to be replaced by objects of class B without causing any errors or changing the desirable behavior of the program.

The key idea is that subtypes must adhere to the contract of their base type. If a subclass deviates from the behavior of the base class in a way that violates the base class’s expectations, then LSP is violated. This leads to problems like unexpected behavior or errors when you try to substitute the subclass where the base class is expected.

Violation of LSP

Let’s assume the Car class has a method refuel(), which fills the tank with gasoline. However, for an electric car, this concept doesn’t make sense because it doesn’t have a fuel tank, but instead, it has a battery that needs charging.

If we are not careful, this difference in behavior could lead to a violation of LSP because the ElectricCar class cannot correctly implement the behavior of refuel() that makes sense for the Car class.

Code for LSP Violation

        // Base Class: Car
        class Car {
            public void refuel() {
                System.out.println("Filling the fuel tank...");
            }
        }
        
        // Subclass: ElectricCar
        class ElectricCar extends Car {
            @Override
            public void refuel() {
                // Electric cars don't use fuel, so we throw an exception
                throw new UnsupportedOperationException("Electric cars don't have fuel tanks!");
            }
        }
        
        // Method to test LSP violation
        public class Main {
            public static void refuelCar(Car car) {
                car.refuel();  // Expect this method to work for any 'Car'
            }
        
            public static void main(String[] args) {
                Car myCar = new Car();
                refuelCar(myCar);  // Works fine
        
                Car myElectricCar = new ElectricCar();
                refuelCar(myElectricCar);  // Throws an exception, violating LSP
            }
        }
        
    

Explanation of the LSP Violation:

In the above code, the method refuel() is part of the Car class, which implies that all Car subclasses should support this behavior. However, the ElectricCar class throws an exception when refuel() is called, which violates the Liskov Substitution Principle. This happens because the ElectricCar class doesn’t behave as expected when substituted for a Car.

The client code (refuelCar() method) expects all Car objects, including ElectricCar, to be refueled, but ElectricCar breaks this assumption.

How to Achieve LSP

To achieve LSP compliance, we need to rethink the design so that each subclass correctly represents the behavior of its superclass. One way to solve this is by splitting the Car class into two separate abstractions: one for fuel-powered cars and one for electric-powered cars. We can introduce a common base class (or interface) like Vehicle that contains shared behavior, and then separate the logic for refueling and recharging into different subclasses.

Refactored Code Following LSP:

        // Base Class: Vehicle
        abstract class Vehicle {
            public abstract void start();
        }
        
        // Fuel-Powered Car
        class FuelCar extends Vehicle {
            @Override
            public void start() {
                System.out.println("Starting fuel car...");
            }
        
            public void refuel() {
                System.out.println("Filling the fuel tank...");
            }
        }
        
        // Electric Car
        class ElectricCar extends Vehicle {
            @Override
            public void start() {
                System.out.println("Starting electric car...");
            }
        
            public void recharge() {
                System.out.println("Recharging the battery...");
            }
        }
        
        // Method to demonstrate LSP
        public class Main {
            public static void main(String[] args) {
                Vehicle myFuelCar = new FuelCar();
                myFuelCar.start();  // Works fine
        
                FuelCar fuelCar = new FuelCar();
                fuelCar.refuel();  // Works fine for FuelCar
        
                Vehicle myElectricCar = new ElectricCar();
                myElectricCar.start();  // Works fine
        
                ElectricCar electricCar = new ElectricCar();
                electricCar.recharge();  // Works fine for ElectricCar
            }
        }
    
Key Points in the Refactored Design:
  1. Common Base Class (Vehicle): Both FuelCar and ElectricCar now extend Vehicle, which provides shared behavior (start() method).
  2. Specific Behaviors in Subclasses: The FuelCar class provides a refuel() method, while the ElectricCar class provides a recharge() method. Now, each class has behavior specific to its type without violating the contract of its parent class.