Dezign Patterns

Liskov Substitution Principle (LSP)

📜 Definition : Subtypes must be replaceable for their base types without breaking functionality.
🎯 Intent : If class B is a subclass of A, you should be able to use B wherever you use A without errors or unexpected behavior.
✅ Good example:

class Bird {
    void fly() {}
}
class Sparrow extends Bird {} // OK ✅

❌ liskov Substitution Principle Violation Example

🧠 General Rule of Thumb:
If your subclass implemneted in such a way where it breaks the parent class/interface contract, it's probably violating LSP. 👀
Note : Follow ISP to avoid bloated interfaces, but don't over-segment interfaces to the point where code becomes fragmented and unmaintainable.


class Ostrich extends Bird {
    void fly() {
        throw new UnsupportedOperationException();  // Ostrich can't fly!
    }
}
This violates LSP — subclass breaks behavior of parent.

In Simple words base class is a Specification which says how it needs to be implemented, If a subclass is implemented by applying some other logic, then we will not be able to replace parent , which leads to break of LSP

Liskov Substitution Principle can be violated in various ways, including

  • incompatible method signatures
  • weakening preconditions
  • changing inherited behavior
  • introducing state inconsistencies.
To fix violations: Ensure subclasses follow the behavior and contract of the superclass, add additional checks or restrictions where necessary, and avoid breaking expectations set by the parent class.

I ) Weakening Preconditions As per the contract of BankAccount class there is no restriction on withdraw logic CheckingAccount kept a condition that withdraw can't be more than 1000

 class BankAccount {
     public void withdraw(double amount) {
         // Withdrawal logic
     }
 }

 class CheckingAccount extends BankAccount {
     @Override
     public void withdraw(double amount) {
         if (amount > 1000) {
             throw new IllegalArgumentException("Cannot withdraw more than $1000 from Checking Account.");
         }
         super.withdraw(amount);
     }
 }
II) Weakening Post Conditions A subclass provides a less strict or weaker post condition (the outcome after method execution) compared to the superclass. Problem: If a subclass guarantees a less restrictive outcome than the superclass, client code relying on the superclass's post conditions might break when the subclass is substituted.

 class PaymentProcessor {
     public void processPayment() { System.out.println("Payment Processed"); }
 }

 class RefundProcessor extends PaymentProcessor {
     @Override
     public void processPayment() {
         // Refund logic that may fail
         throw new UnsupportedOperationException("Refunds not supported here");
     }
 }
III) Overriding Methods and Changing the Behavior of Superclass Contracts

  Account {
      deposit()
      withdraw()
  }

  Saving implements Account {
  }
  Current implements Account {
  }
  Fixed implements Account {
      withdraw() {
          throws Unsupported Op Expression // braks LSP , solve it using interface seggrigation
      }
  }
IV) Introducing state inconsistencies

class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int w) {
        this.width = w;
    }

    public void setHeight(int h) {
        this.height = h;
    }

    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        this.width = w;
        this.height = w;  // Forces height = width
    }

    @Override
    public void setHeight(int h) {
        this.width = h;
        this.height = h;  // Forces width = height
    }
}


public class TestLSP {
    public static void main(String[] args) {
        Rectangle rect = new Square();
        rect.setWidth(5);
        rect.setHeight(10);

        System.out.println("Expected area = 5 * 10 = 50");
        System.out.println("Actual area = " + rect.getArea());
    }
}

Instead of making Square extend Rectangle, prefer composition over inheritance, or use separate classes/interfaces if they have different behavior contracts.
interface Shape {
    int getArea();
}

* Strengthening post condition (ALLOWED)

 class BankAccount {
     private double balance;
     public BankAccount(double initialBalance) {
         this.balance = initialBalance;
     }
     // Deposit method guarantees that the balance will be greater than or equal to the original balance
     public void deposit(double amount) {
         if (amount > 0) {
             balance += amount;
         }
     }
     public double getBalance() {
         return balance;
     }
 }
After a deposit method call, the balance is guaranteed to be greater than or equal to the original balance.

class PremiumAccount extends BankAccount {

    public PremiumAccount(double initialBalance) {
        super(initialBalance);
    }

    // Deposit method guarantees that the balance will be strictly greater than the original balance
    @Override
    public void deposit(double amount) {
        super.deposit(amount);  // Call the superclass method
        if (amount > 500) {
            // Apply a special bonus for large deposits in PremiumAccount
            double bonus = amount * 0.05;
            super.deposit(bonus);  // Add bonus to balance
        }
    }
}
The PremiumAccount guarantees that after the deposit method call, the balance will be strictly greater than the original balance (because it adds a bonus if the deposit is above a certain threshold)