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 fromVehicle
.The
refuel()
method inVehicle
assumes all vehicles use fuel.This violates LSP because
ElectricCar
cannot be refueled and attempting to callrefuel()
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 themove()
method.FueledVehicle
is an abstract class that inherits fromVehicle
and implements therefuel()
method for vehicles that use fuel.ElectricCar
implements theVehicle
interface directly and defines its ownmove()
method for electric cars (using charge level).This ensures all vehicles can be used with the
move()
method, butElectricCar
doesn't have arefuel()
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