27 Oct 2023
Examples of Violations of Liskov Substitution Principle (LSP) along with their corresponding solutions:
Example 1
Problem:
Consider a company that has a class hierarchy for its employees, such as Employee
, Manager
, and Engineer
. The Employee
class has a method called promote()
. The Manager
and Engineer
classes override the promote()
method to implement their own promotion behavior.
class Employee {
public void promote() {
System.out.println("Promoting employee...");
}
}
class Manager extends Employee {
@Override
public void promote() {
System.out.println("Promoting manager...");
}
}
class Engineer extends Employee {
@Override
public void promote() {
System.out.println("Promoting engineer...");
}
}
// Client code
Employee employee = new Manager();
employee.promote(); // Prints "Promoting manager..."
employee = new Engineer();
employee.promote(); // Prints "Promoting engineer..."
This code snippet violates the LSP because a Manager
object cannot be substituted for an Engineer
object without breaking the application. The promote()
method in the Engineer
class may not be applicable to a manager.
Solution:
To address the violation, we can restructure the class hierarchy by introducing a more specific superclass, such as PromotableEmployee
. The promote()
method can then be placed in the relevant subclasses.
class Employee {
}
class PromotableEmployee extends Employee {
public abstract void promote();
}
class Manager extends PromotableEmployee {
@Override
public void promote() {
System.out.println("Promoting manager...");
}
}
class Engineer extends PromotableEmployee {
@Override
public void promote() {
System.out.println("Promoting engineer...");
}
}
// Client code
PromotableEmployee employee = new Manager();
employee.promote(); // Promotes the manager.
employee = new Engineer();
employee.promote(); // Promotes the engineer.
This solution adheres to the LSP because Manager
and Engineer
objects can be substituted for PromotableEmployee
objects without causing issues.
Example 2
Problem:
Consider a class hierarchy for different types of food, such as Food
, Fruit
, and Vegetable
. The Food
class has a method called eat()
. The Fruit
and Vegetable
classes override the eat()
method.
class Food {
public void eat() {
System.out.println("Eating food...");
}
}
class Fruit extends Food {
@Override
public void eat() {
System.out.println("Eating fruit...");
}
}
class Vegetable extends Food {
@Override
public void eat() {
System.out.println("Eating vegetable...");
}
}
// Client code
Food food = new Fruit();
food.eat(); // Prints "Eating fruit..."
food = new Vegetable();
food.eat(); // Prints "Eating vegetable..."
This code snippet violates the LSP because a Vegetable
object cannot be substituted for a Fruit
object without potentially causing issues. Not all vegetables are suitable for eating raw.
Solution:
To rectify the issue, we can create a more specific superclass, such as EdibleFood
, and move the eat()
method there. This way, the subclasses, Fruit
and Vegetable
, can implement their specific eating behaviors.
class Food {
}
class EdibleFood extends Food {
public abstract void eat();
}
class Fruit extends EdibleFood {
@Override
public void eat() {
System.out.println("Eating fruit...");
}
}
class Vegetable extends EdibleFood {
@Override
public void eat() {
System.println("Eating vegetable...");
}
}
// Client code
EdibleFood food = new Fruit();
food.eat(); // Prints "Eating fruit..."
food = new Vegetable();
food.eat(); // Prints "Eating vegetable..."
This solution adheres to the LSP because Fruit
and Vegetable
objects can be used interchangeably with EdibleFood
objects without issues.
Example 3
Problem:
Consider a class hierarchy for animals, such as Animal
, Bird
, and Penguin
. The Animal
class has a method called makeSound()
, which is overridden by Bird
and Penguin
.
class Animal {
public void makeSound() {
System.out.println("Making animal sound...");
}
}
class Bird extends Animal {
@Override
public void makeSound() {
System.out.println("Chirping...");
}
}
class Penguin extends Bird {
@Override
public void makeSound() {
System.out.println("Honking...");
}
}
// Client code
Animal animal = new Bird();
animal.makeSound(); // Prints "Chirping..."
animal = new Penguin();
animal.makeSound(); // Prints "Honking..."
This code snippet violates the LSP because a Penguin
object cannot be substituted for a Bird
object without potentially causing issues. Penguins don't chirp; they make a different sound.
Solution:
To resolve the violation, we can introduce a more specific superclass, such as SoundMakingAnimal
, and place the makeSound()
method there. The subclasses, Bird
and Penguin
, can then implement their specific sound-making behaviors.
class Animal {
}
class SoundMakingAnimal extends Animal {
public abstract void makeSound();
}
class Bird extends SoundMakingAnimal {
@Override
public void makeSound() {
System.out.println("Chirping...");
}
}
class Penguin extends SoundMakingAnimal {
@Override
public void makeSound() {
System.out.println("Honking...");
}
}
// Client code
SoundMakingAnimal animal = new Bird();
animal.makeSound(); // Prints "Chirping..."
animal = new Penguin();
animal.makeSound(); // Prints "Honking..."
This solution adheres to the LSP because Bird
and Penguin
objects can be used interchangeably with SoundMakingAnimal
objects without causing issues.
Example 4
Problem:
Consider a class hierarchy for vehicles, including Vehicle
, Car
, and Airplane
. The Vehicle
class has a method called drive()
, and both Car
and Airplane
override this method.
class Vehicle {
public void drive() {
System.out.println("Driving a vehicle...");
}
}
class Car extends Vehicle {
@Override
public void drive() {
System.out.println("Driving a car...");
}
}
class Airplane extends Vehicle {
@Override
public void drive() {
System.out.println("Flying a plane...");
}
}
// Client code
Vehicle vehicle = new Car();
vehicle.drive(); // Prints "Driving a car..."
vehicle = new Airplane();
vehicle.drive(); // Prints "Flying a plane..."
This code snippet violates the LSP because an Airplane
object cannot be used in place of a Vehicle
object without potentially causing issues. The drive()
method in the Vehicle
class doesn't apply to an airplane.
Solution:
To rectify the violation, we can create a more specific superclass, such as LandVehicle
, and move the drive()
method there. Additionally, we introduce a new subclass, FlyingVehicle
, to
implement the flying behavior for airplanes.
class Vehicle {
}
class LandVehicle extends Vehicle {
public void drive() {
System.out.println("Driving a land vehicle...");
}
}
class Car extends LandVehicle {
}
class Airplane extends FlyingVehicle {
public void fly() {
System.out.println("Flying a plane...");
}
}
class FlyingVehicle extends Vehicle {
public void fly() {
System.out.println("Flying a vehicle...");
}
}
// Client code
Vehicle vehicle = new Car();
vehicle.drive(); // Prints "Driving a land vehicle..."
vehicle = new Airplane();
vehicle.fly(); // Prints "Flying a plane..."
This solution adheres to the LSP because an Airplane
object cannot be used in place of a LandVehicle
object. The drive()
method is specific to LandVehicle
.
Example 5
Problem:
Consider a class hierarchy for different types of vehicles, such as Vehicle
, Car
, and Truck
. The Vehicle
class has a method called load()
, which is overridden by Car
and Truck
.
class Vehicle {
public void load() {
System.out.println("Loading vehicle...");
}
}
class Car extends Vehicle {
@Override
public void load() {
System.out.println("Loading car with passengers and luggage...");
}
}
class Truck extends Vehicle {
@Override
public void load() {
System.out.println("Loading truck with cargo...");
}
}
// Client code
Vehicle vehicle = new Car();
vehicle.load(); // Prints "Loading car with passengers and luggage..."
vehicle = new Truck();
vehicle.load(); // Prints "Loading truck with cargo..."
This code snippet violates the LSP because a Truck
object cannot be used in place of a Car
object without potentially causing issues. The load()
method in the Car
class doesn't apply to a truck's cargo loading.
Solution:
To resolve the violation, we can introduce a more specific superclass, such as LoadableVehicle
, and place the load()
method there. The subclasses, Car
and Truck
, can then implement their specific loading behaviors.
class Vehicle {
}
class LoadableVehicle extends Vehicle {
public abstract void load();
}
class Car extends LoadableVehicle {
@Override
public void load() {
System.out.println("Loading car with passengers and luggage...");
}
}
class Truck extends LoadableVehicle {
@Override
public void load() {
System.out.println("Loading truck with cargo...");
}
}
// Client code
LoadableVehicle vehicle = new Car();
vehicle.load(); // Loads the car with passengers and luggage.
vehicle = new Truck();
vehicle.load(); // Loads the truck with cargo.
This solution adheres to the LSP because Car
and Truck
objects can be used interchangeably with LoadableVehicle
objects without causing issues.
Here is a short summary of some of the key points about the LSP:
- The LSP is violated when a subclass cannot be used in place of its superclass without breaking the program.
- There are a number of ways to fix LSP violations, such as refactoring the class hierarchy or using interfaces.
- The LSP helps to ensure that code is more maintainable and reusable.