SOLID Principles: Liskov substitution Principle (LSP)

SOLID Principles: Liskov substitution Principle (LSP)

Introduction

LSP focuses on the concept of inheritance, a fundamental building block in object-oriented programming (OOP). It ensures that a subclass (a more specific type) can be seamlessly substituted for its parent class (a more general type) without causing unexpected behavior or breaking the program.

Key Ideas

Focus on Inheritance:

  • LSP specifically applies to inheritance hierarchies, where classes inherit properties and methods from their parent classes.

Subclasses as Replacements:

  • The core principle states that objects of a subclass should be replaceable with objects of its parent class without altering the program's correctness.

Maintaining Contracts:

  • Subclasses shouldn't weaken the preconditions (expected inputs) or postconditions (expected outputs) established by the parent class. In other words, they shouldn't introduce stricter requirements for using the methods or generate unexpected results.

Avoiding Breakage:

  • Subclasses shouldn't introduce new exceptions or modify the behavior of existing exceptions compared to the parent class. This ensures code that works with the parent class continues to function predictably with subclasses.

Bad Example (Not Following) LSP

Imagine we have an object-oriented program for a Vehicle:

class Vehicle {
  private String model;
  private int fuelLevel;

  public Vehicle(String model) {
    this.model = model;
    this.fuelLevel = 100; // Assuming initial fuel level
  }

  public void refuel(int amount) {
    fuelLevel += amount;
    System.out.println("Refueling " + model + "...");
  }

  public void move() {
    if (fuelLevel > 0) {
      fuelLevel -= 10; // Simulates fuel consumption
      System.out.println(model + " is moving...");
    } else {
      System.out.println(model + " is out of fuel!");
    }
  }
}

class ElectricCar extends Vehicle { // Violation

  public ElectricCar(String model) {
    super(model);
  }

  @Override
  public void refuel(int amount) {
    throw new UnsupportedOperationException("Electric cars cannot be refueled!");
  }
}

public class Main {
  public static void main(String[] args) {
    Vehicle car = new Vehicle("Gasoline Car");
    ElectricCar electricCar = new ElectricCar("Electric Car");

    car.move();   // Output: Gasoline Car is moving...
    car.refuel(20); // Output: Refueling Gasoline Car...

    electricCar.move();  // Output: Electric Car is moving...
    electricCar.refuel(20); // Throws UnsupportedOperationException  (Violation)
  }
}

Problem:

  • ElectricCar inherits from Vehicle.

  • The refuel() method in Vehicle assumes all vehicles use fuel.

  • This violates LSP because ElectricCar cannot be refueled and attempting to call refuel() on it throws an exception.

This violates the principle stating that objects of a subclass should be replaceable with objects of its parent class without altering the program's correctness.

Unexpected Behavior: When electricCar.refuel(20); is called, the program expects a behavior consistent with a refuel, but an error is thrown.

Improved Version

interface Vehicle {
  public void move();
}

public abstract class FueledVehicle implements Vehicle {
  private String model;
  private int fuelLevel;

  public FueledVehicle(String model) {
    this.model = model;
    this.fuelLevel = 100; // Assuming initial fuel level
  }

  public void refuel(int amount) {
    fuelLevel += amount;
    System.out.println("Refueling " + model + "...");
  }

  @Override
  public abstract void move(); // Abstract method, subclasses must define fuel consumption logic
}

public class ElectricCar implements Vehicle {
  private String model;
  private int chargeLevel;

  public ElectricCar(String model) {
    this.model = model;
    this.chargeLevel = 100; // Assuming initial charge level
  }

  @Override
  public void move() {
    if (chargeLevel > 0) {
      chargeLevel -= 10; // Simulates charge consumption
      System.out.println(model + " is moving...");
    } else {
      System.out.println(model + " is out of charge!");
    }
  }
}

public class Main {
  public static void main(String[] args) {
    Vehicle car = new FueledVehicle("Gasoline Car");
    ElectricCar electricCar = new ElectricCar("Electric Car");

    car.move();   // Output: Gasoline Car is moving...
    ((FueledVehicle) car).refuel(20); // Explicit cast for refueling (optional)
                                     // Output: Refueling Gasoline Car...

    electricCar.move();  // Output: Electric Car is moving...
    electricCar.refuel(20); // No refueling method for ElectricCar (Correct behavior)
  }
}
  • Vehicle is now an interface that defines the move() method.

  • FueledVehicle is an abstract class that inherits from Vehicle and implements the refuel() method for vehicles that use fuel.

  • ElectricCar implements the Vehicle interface directly and defines its own move() method for electric cars (using charge level).

  • This ensures all vehicles can be used with the move() method, but ElectricCar doesn't have a refuel() method, adhering to LSP.

Benefits of LSP

  • Improved Reliability: Code becomes more reliable as subclasses won't introduce unexpected behavior when used interchangeably with the parent class.

  • Enhanced Maintainability: Changes to the parent class are less likely to break existing code that relies on functionalities of subclasses.

  • Increased Flexibility: New functionalities can be added through subclasses without disrupting existing code that interacts with the parent class.


And with the Liskov Substitution Principle (LSP) firmly grasped, we can now confidently rely on subclasses to seamlessly replace their parent classes without introducing unexpected behavior. This allows for a clean and predictable codebase, primed for future enhancements. But our journey towards robust and adaptable software doesn't end here. Buckle up again, because we're about to delve into the Interface Segregation Principle (ISP)!


Related Articles

SOLID Principles: Single Responsibility

SOLID Principles: Open Closed Principle

YouTube

Previous Videos

SOLID Principles: Single Responsibility Principle

Clean Code: Meaningful Names

Clean Code: A Demystified Intro