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.