Monday, December 9, 2024

Liskov Substitution Principle (LSP) Violation Example

For example:A "Lion" (a derived class of "Mammal") can be substituted for an "Animal" in the program, and it can eat, sleep, and make sounds just like any other animal.

A "Penguin" (a derived class of "Bird") should also be substitutable for an "Animal" and exhibit the same basic behaviors.

class Animal {
    public void eat() {
        System.out.println("The animal is eating.");
    }
    
    public void sleep() {
        System.out.println("The animal is sleeping.");
    }
    
    public void makeSound() {
        System.out.println("The animal makes a sound.");
    }
}

class Lion extends Animal {
    @Override
    public void makeSound() {
        System.out.println("The lion roars!");
    }
}

class Penguin extends Animal {
    @Override
    public void makeSound() {
        System.out.println("The penguin squawks.");
    }
}

public class Zoo {
    public static void main(String[] args) {
        Animal genericAnimal = new Animal();
        Animal lionAnimal = new Lion();
        Animal penguinAnimal = new Penguin();
        
        genericAnimal.eat();
        genericAnimal.sleep();
        genericAnimal.makeSound();
        
        lionAnimal.eat();
        lionAnimal.sleep();
        lionAnimal.makeSound();
        
        penguinAnimal.eat();
        penguinAnimal.sleep();
        penguinAnimal.makeSound();
    }
}


Key Idea of LSP

The LSP says:

If you have a base class (Animal), you should be able to replace it with any of its subclasses (Lion, Penguin) without breaking the functionality of your program.

A violation happens when the subclass changes behavior in a way that:

  1. Introduces unexpected restrictions or exceptions.
  2. Breaks the assumptions made in the base class (Animal).
  3. Forces clients (code that uses Animal) to handle subclasses differently.

LSP Violation 1: Adding Restrictions

Example: Lion changes eat behavior

In the base class (Animal), the eat method is assumed to work for all animals without restrictions.

If the Lion class overrides eat and introduces a restriction (e.g., lions only eat meat), the program might break when a Lion is substituted for an Animal.

class Lion extends Animal {
    @Override
    public void eat() {
        throw new UnsupportedOperationException("Lions can only eat meat!");
    }
}

// Test Case
Animal animal = new Lion(); // Substituting Lion for Animal
animal.eat(); // Throws an exception!

Why This Breaks LSP:

  • The code expects eat() to work for all Animal objects.
  • A Lion violates this expectation by restricting what it can eat.

LSP Violation 2: Changing Return Type

If the subclass (Lion) changes the return type of a method, it will break polymorphism and substitutability.

Example: Lion's makeSound returns a String instead of void

class Lion extends Animal {
    @Override
    public String makeSound() { // Invalid override
        return "Roar!";
    }
}

// Test Case
Animal animal = new Lion(); // Substituting Lion for Animal
animal.makeSound(); // Compilation error! Method signature mismatch.

Why This Breaks LSP:

  • The base class defines makeSound() as returning void.
  • A subclass (Lion) changes the return type, making it incompatible with the base class.

LSP Violation 3: Changing Preconditions

If a subclass (Penguin) introduces stricter conditions on when a method can be used, it breaks LSP.

Example: Penguin changes sleep behavior

class Penguin extends Animal {
    @Override
    public void sleep() {
        if (!isNightTime()) {
            throw new IllegalStateException("Penguins only sleep at night!");
        }
        super.sleep();
    }

    private boolean isNightTime() {
        return false; // Simulating day time
    }
}

// Test Case
Animal animal = new Penguin(); // Substituting Penguin for Animal
animal.sleep(); // Throws IllegalStateException!

Why This Breaks LSP:

  • The base class (Animal) promises that all animals can sleep at any time.
  • The subclass (Penguin) introduces a condition that breaks this promise.

LSP Violation 4: Violating Postconditions

Postconditions are guarantees made by the base class after a method is called. If a subclass weakens these guarantees, it violates LSP.

Example: Lion changes makeSound behavior

In the base class, makeSound guarantees that every animal produces a sound. If Lion violates this by failing to make a sound, it breaks LSP.

class Lion extends Animal {
    @Override
    public void makeSound() {
        // Breaks the guarantee that all animals make a sound
        throw new UnsupportedOperationException("This lion is silent!");
    }
}

// Test Case
Animal animal = new Lion(); // Substituting Lion for Animal
animal.makeSound(); // Throws exception!

Why This Breaks LSP:

  • The base class (Animal) promises that all animals will make a sound.
  • The subclass (Lion) breaks this promise, causing unexpected behavior.

Key Points to Remember

  • Base Class Assumptions: The base class (Animal) makes certain promises (e.g., eat, sleep, and makeSound always work without extra restrictions).
  • Substituting Subclasses: Subclasses (Lion, Penguin) should fulfill those promises without adding stricter requirements or removing functionality.
  • Breaking LSP: If a subclass changes behavior (e.g., adds restrictions, changes return types, or fails to meet expectations), it breaks LSP.

How to Avoid LSP Violations

  1. Follow the Base Class Contract: Ensure that subclasses respect the behavior defined in the base class.
  2. Avoid Strengthening Preconditions: Don’t add new requirements (e.g., time of day) in subclass methods.
  3. Avoid Weakening Postconditions: Ensure that subclasses meet the guarantees promised by the base class.
  4. Use Composition Over Inheritance: If the subclass behavior is too different, consider using composition instead of inheritance.


No comments:

Post a Comment

LeetCode C++ Cheat Sheet June

🎯 Core Patterns & Representative Questions 1. Arrays & Hashing Two Sum – hash map → O(n) Contains Duplicate , Product of A...