SOLID Principles: Liskov substitution Principle (LSP)

Building Pyramids of Code: A Tech Leader from Cairo
I hail from the land of the Great Pyramids, where I've traded ancient wonders for the ever-evolving marvels of software. As Head of Software Engineering, my days are a captivating blend of the Nile's serenity and the frenetic energy of building cutting-edge tech solutions.
Cairo's vibrant tech scene is my playground. I've navigated its bustling hubs, honed my skills at startups and consultancies, and witnessed firsthand the power of technology to transform lives. This journey has instilled in me a deep understanding of the region's unique needs and opportunities.
My leadership philosophy? Think straight-talking Sphinx – I guide with unwavering honesty, challenging directly while caring fiercely. No sugarcoating, no backstabbing, just clear feedback and genuine support. We build pyramids of code, not walls of BS.
My team thrives in this arena of radical candor. We rip through roadblocks with open communication, celebrate wins loudly, and learn from failures even faster. It's messy, sometimes brutal, but always authentic. Because in the crucible of challenging directly, true brilliance emerges. I empower my teams to unleash their creativity, fostering a culture of innovation where ideas flow like the Nile. We tackle complex challenges, from scaling platforms to crafting intuitive interfaces, all while embracing the dynamic spirit of Cairo.
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:
ElectricCarinherits fromVehicle.The
refuel()method inVehicleassumes all vehicles use fuel.This violates LSP because
ElectricCarcannot 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)
}
}
Vehicleis now an interface that defines themove()method.FueledVehicleis an abstract class that inherits fromVehicleand implements therefuel()method for vehicles that use fuel.ElectricCarimplements theVehicleinterface directly and defines its ownmove()method for electric cars (using charge level).This ensures all vehicles can be used with the
move()method, butElectricCardoesn'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