CLOSE

What is Liskov Substitution Principle (LSP)?

In simpler terms, this principle advocates that objects of a superclass should be replaceable with objects of its subclass without affecting the correctness of the program.

It states that: Objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program.

📜 Formal Definition

Barbara Liskov introduced this principle in 1987:

“If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program.”

🧠 In Simple Terms

If a class Child is derived from Parent, then you should be able to use Child wherever you use Parent – without surprises or breaking the code.

In other words, A subclass should not only inherit from the superclass, but also behave like the superclass

Key Concepts

  1. Behavioral Compatibility: A subclass must exhibit the behavior expected by the parent class. It should not introduce unexpected behavior.
  2. Contract Preservation: Subclasses must adhere to the "contract" defined by the parent class (method definitions, preconditions, postconditions, invariants).
  3. No Side Effects: Substitution should not lead to program errors, misbehavior, or inconsistencies.

🚫 LSP Violation Example (Problem) - Rectangle and Square

Consider an example where Rectangle is a parent class, and Square is a subclass:

class Rectangle {
protected:
    int width;
    int height;
public:
    void setWidth(int w) { width = w; }
    void setHeight(int h) { height = h; }
    int getArea() { return width * height; }
};

class Square : public Rectangle {
public:
    void setWidth(int w) override { 
        width = w; 
        height = w; // Ensure square's sides are equal
    }
    void setHeight(int h) override { 
        width = h; 
        height = h; // Ensure square's sides are equal
    }
};

// Client code
void printArea(Rectangle& rect) {
    rect.setWidth(5);
    rect.setHeight(10);
    cout << "Area: " << rect.getArea() << endl; // Expect 50
}

int main() {
    Rectangle rect;
    printArea(rect); // Works as expected

    Square sq;
    printArea(sq); // Output is incorrect because setWidth affects height
    return 0;
}

⚠️ Why This Violates LSP:

  1. Behavioral Mismatch: The client code expects the area to be computed as width * height. However, Square overrides these methods to maintain equal sides, which disrupts the expected behavior.
  2. Substitution Breaks: Replacing Rectangle with Square causes logical errors in the program.

✅ LSP Compliant Design (Solution)

To adhere to LSP, we can redesign the hierarchy to avoid such conflicts. One way is to avoid inheriting Square from Rectangle and instead use composition.

Correct Design: Composition Instead of Inheritance

class Shape {
public:
    virtual int getArea() const = 0;
    virtual ~Shape() = default;
};

class Rectangle : public Shape {
protected:
    int width;
    int height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    void setWidth(int w) { width = w; }
    void setHeight(int h) { height = h; }
    int getArea() const override { return width * height; }
};

class Square : public Shape {
protected:
    int side;
public:
    Square(int s) : side(s) {}
    void setSide(int s) { side = s; }
    int getArea() const override { return side * side; }
};

// Client code
void printArea(Shape& shape) {
    cout << "Area: " << shape.getArea() << endl;
}

int main() {
    Rectangle rect(5, 10);
    printArea(rect); // Output: Area: 50

    Square sq(5);
    printArea(sq); // Output: Area: 25
    return 0;
}

Why This Works:

  1. No Inheritance Conflicts: Rectangle and Square are independent implementations of the Shape interface.
  2. Behavioral Compatibility: Each class maintains its unique behavior without violating client expectations.

🐦 Another Example — Birds That Can Fly (or Not)

Let's consider an example of a Bird class hierarchy that includes different types of birds, such as Eagle, Penguin, and Ostrich. Each bird has a different ability to fly.

class Bird {
public:
  virtual void fly() = 0;
};

class Eagle : public Bird {
public:
  void fly() override;
};

class Penguin : public Bird {
public:
  void fly() override;
};

class Ostrich : public Bird {
public:
  void fly() override;
};

Now, let's suppose that we have a function that takes a Bird object and makes it fly.

void makeBirdFly(Bird& bird) {
  bird.fly();
}

According to the LSP, we should be able to pass any Bird subclass object to this function without affecting the correctness of the program. For example, we should be able to pass an Eagle object, a Penguin object, or an Ostrich object to this function.

Eagle eagle;
Penguin penguin;
Ostrich ostrich;

makeBirdFly(eagle); // okay, eagle can fly
makeBirdFly(penguin); // error, penguin can't fly
makeBirdFly(ostrich); // error, ostrich can't fly

As we can see, passing a Penguin or an Ostrich object to this function would result in an error because they cannot fly. Therefore, these classes do not adhere to the same behavior as their parent class, Bird, and violate the LSP.

Example:

Imagine we have a set of geometric shapes: Rectangle, Shape, and Circle. All these shapes have a area calculation method:

#include <iostream>

class Shape {
public:
    virtual double area() const = 0;
};

class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double width, double height){
        this->width = width;
        this->height = height;
    }
    double area() const override {
        return width * height;
    }
};

class Square : public Shape {
private:
    double side;
public:
    Square(double side){
        this->side = side;
    }
    double area() const override {
        return side * side;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double radius){
        this->radius = radius;
    }
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

Here, Rectangle, Square, and Circle all inherit from Shape and implement the area() method. This adheres to LSP because each shape can be substituted for another in contexts where the Shape base class is expected. For example:

void printArea(const Shape& shape) {
    std::cout << "Area: " << shape.area() << std::endl;
}

int main() {
    Rectangle rect{4, 5};
    Square square{4};
    Circle circle{3};

    printArea(rect);   // Output: Area: 20
    printArea(square); // Output: Area: 16
    printArea(circle); // Output: Area: 28.27431

    return 0;
}

In this example, we can see that Rectangle, Square, and Circle objects are seamlessly substituting Shape objects in the printArea() function without affecting the correctness of the program, thus satisfying the LSP.

Guidelines of LSP

1️⃣ Signature Rules (Method Signature Rules)

These rules ensure that interface of the subclass is compatible with the superclass.

Rule 1: Method Argument Rule

The overridden method in the subclass must accept the same or more general (wider) types as arguments — meaning it should be compatible with the base class method signature so that the subclass can be used interchangeably with the base class.

It states that the overiden method in the sub class must accept the same argument types as the parent, or wider (a broader type up in the inheritance chain).

Example: If the parent method takes a string, the child override must also take string (or a supertype, e.g. object) never an unrelated type like int.

🔍 Why This Matters

If the subclass requires more specific or different types than the base class, code that uses the base class will break when a subclass is passed — violating substitutability.

❌  Bad Example (Violates LSP)

class Animal {
public:
    virtual void eat(std::string food) {}
};

class Dog : public Animal {
public:
    void eat(int kibbleCount) {}  // ⚠️ Violates LSP: incompatible signature
};
  • This is not a valid override – it's a different method (name overloading, not overriding).
  • Replacing Animal* a = new Dog(); a->eat("meat"); will fail.

✅ Good Example (Follows LSP)

class Animal {
public:
    virtual void eat(const std::string& food) {}
};

class Dog : public Animal {
public:
    void eat(const std::string& food) override {}
};
  • Subclass accepts the same type as the base class – this is a valid override and safe substitution.

⚠️ Note on Wider Types in C++

C++ doesn’t allow changing parameter types in overrides — even to a supertype — because C++ requires exact signature match for virtual function overrides. The idea of "wider types" is more applicable in languages with runtime polymorphism like Java or C#, where you might use Object as a generic supertype.

Complete Example:

#include <iostream>

using namespace std;

// Method Argument Rule : 
// Subtype method arguments can be identical or wider than the supertype
// C++ imposes this by keeping singature identical

class Parent {
public:
    virtual void print(string msg) {
        cout << "Parent: " << msg << endl;
    }
};

class Child : public Parent {
public:
    void print(string msg) override { 
        cout << "Child: " << msg << endl;
    }
};

//Client that pass string as msg as client expects.
class Client {
private:
    Parent* p;

public:
  Client(Parent* p) {
        this->p = p;
    }  
    void printMsg() {
        p->print("Hello");
    }
};

int main() {

    Parent* parent = new Parent();
    Parent* child = new Child();

    //Client* client = new Client(parent);
    Client* client = new Client(child);

    client->printMsg();


    return 0;
}

Summary:

RuleLSP StatusNotes
Subclass uses same parameter type✅ ValidProper overriding
Subclass uses a narrower type❌ InvalidNot substitutable
Subclass uses a wider type (in theory)✅ Conceptually validBut not supported in C++ overriding
Subclass changes number of parameters❌ InvalidBreaks the contract

Rule 2: Return Type Rule

The overridden method in the subclass must return the same type or a subtype (more specific) of the return type declared in the parent class.

🧠 Why It Matters

  • If the subclass returns an unrelated or broader type, client code expecting the base type may break or behave incorrectly.
  • This is called covariant return types — allowed in languages like C++, Java, and C#.

✅ Valid Example (Covariant Return Type in C++)

class Animal {
public:
    virtual Animal* clone() {
        return new Animal();
    }
};

class Dog : public Animal {
public:
    Dog* clone() override { // ✅ returns a subclass (Dog*), OK in C++
        return new Dog();
    }
};
  • Dog* is a subtype of Animal*, so the substitution still works.
  • This is a safe override, adhering to LSP.

❌ Invalid Example

class Animal {
public:
    virtual Animal* clone();
};

class Dog : public Animal {
public:
    int clone();  // ❌ Different return type, not valid in C++
};
  • This isn't overriding – it's a new unrelated method.
  • Code expecting Animal* will break when using Dog.

Complete Example:

#include <iostream>

using namespace std;

// Return Type Rule : 
// Subtype overriden method return type should be either identical 
// or narrower then the parent method's return type.
// This is also called as return type covariance.
// C++ enforces this by covariance.

class Animal {
    //some common Animal methods
};

class Dog : public Animal {
    //Additional Dog methods specific to Dogs.
};


class Parent {
public:
    virtual Animal* getAnimal() { 
        cout << "Parent : Returning Animal instance" << endl;
        return new Animal();
    }
};

class Child : public Parent {
public:
//  Can also have return type as Dog
    Animal* getAnimal() override { 
        cout << "Child : Returning Dog instance" << std::endl;
        return new Dog();
    }
};

class Client {
private:
    Parent* p;

public:
    Client(Parent* p) {
        this->p = p;
    }
    void takeAnimal() {
        p->getAnimal();
    }
};

int main() {
    Parent* parent = new Parent();
    Child* child = new Child();

    Client* client = new Client(child);
    //Client * client = new Client(parent);
    client->takeAnimal();

    return 0;
}

📌 Key Takeaways

AspectRuleValid in C++?LSP-compliant?
Same return type✅ Yes✅ Yes 
Subclass return type✅ (Covariant return)✅ Yes 
Unrelated or broader type❌ Not allowed❌ No 
Changed return type❌ Compiles as overload (not override)❌ No 

Rule 3: Exception Rule

A subclass must not throw new, broader, or more exceptions than the method in its base class.

This ensures that code using a base class reference won’t be surprised by unexpected exceptions when a subclass is substituted.

🧠 Why It Matters

If client code is built to handle only the exceptions thrown by the base class method, introducing additional or unchecked exceptions in the subclass will violate substitutability.

It breaks the client’s assumptions and error-handling logic, violating robustness and predictability.

✅ Valid Example

class DataFetcher {
public:
    virtual void fetch() noexcept {  // Promise: no exceptions thrown
        // implementation
    }
};

class NetworkFetcher : public DataFetcher {
public:
    void fetch() noexcept override { // ✅ Keeps the contract
        // implementation
    }
};
  • NetworkFetcher respects the base class guarantee of no exceptions.

Invalid Example

class DataFetcher {
public:
    virtual void fetch() noexcept {  // Caller assumes no exceptions
        // does nothing dangerous
    }
};

class NetworkFetcher : public DataFetcher {
public:
    void fetch() override {
        throw std::runtime_error("Network error");  // ❌ Violates base contract
    }
};
  • Code using DataFetcher is not prepared to handle std::runtime_error.

Complete Example

#include <iostream>

using namespace std;

// Exception Rule:
// A subclass should throw fewer or narrower exceptions 
// (but not additional or broader exceptions) than the parent.
// C++ does not enforces this. Hence no compilation error.

/*
├── std::logic_error        <-- For logical errors detected before runtime
│   ├── std::invalid_argument   <-- Invalid function argument
│   ├── std::domain_error       <-- Function argument domain error
│   ├── std::length_error       <-- Exceeding valid length limits
│   ├── std::out_of_range       <-- Array or container index out of bounds
│
├── std::runtime_error      <-- For errors that occur at runtime
│   ├── std::range_error        <-- Numeric result out of range
│   ├── std::overflow_error     <-- Arithmetic overflow
│   ├── std::underflow_error   
*/

class Parent {
public:
    virtual void getValue() noexcept(false) { // Parent throws logic_error exception
        throw logic_error("Parent error");
    }
};

class Child : public Parent {
public:
    void getValue() noexcept(false)  override { // Child throws out_of_range exception
        throw out_of_range("Child error");
        // throw runtime_error("Child Error"); // This is Wrong
    }
};

class Client {
private:
    Parent* p;

public:
    Client(Parent* p) {
        this->p = p;
    }
    void takeValue() {
        try {
            p->getValue();
        }
        catch(const logic_error& e) {
            cout << "Logic error exception occured : " << e.what() << endl;
        }
    }
};

int main() {
    Parent* parent = new Parent();
    Child* child = new Child();

    Client* client = new Client(parent);
    //Client* client = new Client(child);

    client->takeValue();

    return 0;
}
    

📌 Key Guidelines

PrincipleExplanation
No new exceptionsSubclass should not introduce new exception types
No broader exceptionsDon't replace specific with general (e.g., Exception)
Can throw fewer exceptionsSubclass may choose to throw fewer or none
Respect exception guaranteesIf base says noexcept or similar, subclass must too

2️⃣ Property Rules

Property rules define how state (data members or properties) in a subclass must behave in relation to the base class. Violating these rules breaks the principle of substitutability and causes unexpected behaviors in polymorphic usage.

📜 What Are Property Rules?

Property rules state that subclasses must preserve the valid state and relationships of the base class’s properties — also known as invariants.

This is a critical part of ensuring that a subclass can be substituted for its base class without breaking client code.

Rule 1: Class Invariant

📜 Definition

A class invariant is a condition that must always be true for all valid objects of a class, from the time they are constructed until they are destroyed (except temporarily during internal operations).

In other words, it's a rule about the object's state that:

  • Must be established during construction,
  • Must be maintained by all public methods,
  • And must never be violated after a method finished executing.

🧠 Why Class Invariants Matter

  • They define the correctness of a class.
  • They let other developers safely reason about how the class behaves.
  • They're essential for applying the Liskov Substitution Principle (LSP): subclasses must not violate the base class’s invariants.

Example

#include<iostream>

using namespace std;

// Class Invariant of a parent class Object should not be broken by child class Object.
// Hence child class can either maintain or strengthen the invariant but never narrows it down.

//Invariant : Balance cannot be negative
class BankAccount {
protected:
    double balance;
public:
    BankAccount(double b) {
        if (b < 0) throw invalid_argument("Balance can't be negative");
        balance = b;
    }
    virtual void withdraw(double amount) {
        if (balance - amount < 0) throw runtime_error("Insufficient funds");
        balance -= amount;
        cout<< "Amount withdrawn. Remaining balance is " << balance << endl;
    }
};

//Brakes invariant : Should not be allowed.
class CheatAccount : public BankAccount {
public:
    CheatAccount(double b) : BankAccount(b) {}

    void withdraw(double amount) override {
        balance -= amount; // LSP break! Negative balance allowed, // ❌ No check for negative balance
        cout<< "Amount withdrawn. Remaining balance is " << balance << endl;
    }
};

int main() {
    BankAccount* bankAccount = new BankAccount(100);
    bankAccount->withdraw(100);
}
  • BankAccount class guarantees that:
    • Balance can never be negative.
    • All operations enforce this invariant.
  • This is a contract: Any client using BankAccount expects that balance will never go below zero.
  • While CheatAccount weakens this invariant – it allows balance < 0:
    • Withdraw function allows us to withdraw 200 balance while we only have 100, causing balance to go negative.
    • Hence, it violates the Liskov Substituition Principle.
    • A function expecting a BankAccount would behave incorrectly if passed a CheatAccount.

Rule 2: History Constraints

📜 Definition

History Constraints ensure that the state of an object should evolve over time in a manner consistent with its base class.

In simpler terms:

A subclass should not change how the state of the object behaves or mutates over time compared to its base class.

🧠 Why It Matters

Even if:

  • A subclass respects method signatures,
  • And maintains class invariants,

...it may still violate LSP if it alters how state changes over time in ways the base class would not allow.

📌 Example — Immutable vs Mutable (Violation)

class Point {
protected:
    int x, y;
public:
    Point(int x, int y): x(x), y(y) {}
    virtual void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
};

class ImmutablePoint : public Point {
public:
    ImmutablePoint(int x, int y): Point(x, y) {}
    
    void move(int dx, int dy) override {
        // ❌ Do nothing! State does not evolve like base class
        cout << "Immutable point cannot move!" << endl;
    }
};

❌ Why This Breaks LSP

  • Base class Point has mutable state; it evolves.
  • Subclass ImmutablePoint breaks that behavior: it prevents state change.
  • Client code expecting Point behavior will be confused when move() has no effect.

✅ Valid LSP Respecting Example

class MovingPoint : public Point {
public:
    MovingPoint(int x, int y): Point(x, y) {}

    void move(int dx, int dy) override {
        Point::move(dx, dy); // ✅ same history behavior
    }
};

Complete Example

#include<iostream>

using namespace std;

// Sub class methods should not be allowed state changes What
// Base class never allowed.

class BankAccount {
protected:
    double balance;

public:
    BankAccount(double b) {
        if (b < 0) throw invalid_argument("Balance can't be negative");
        balance = b;
    }

    // History Constraint : Withdraw should be allowed
    virtual void withdraw(double amount) {
        if (balance - amount < 0) throw runtime_error("Insufficient funds");
        balance -= amount;
        cout<< "Amount withdrawn. Remaining balance is " << balance << endl;
    }
};
    

class FixedDepositAccount : public BankAccount {
public:
    FixedDepositAccount(double b) : BankAccount(b) {}

    // LSP break! History constraint broke!
    // Parent class behaviour change : Now withdraw is not allowed.
    //This class will brake client code that relies on withdraw.
    void withdraw(double amount) override {
    	// ❌ Disallowed withdrawal
        throw runtime_error("Withdraw not allowed in Fixed Deposit");
    }
};
    
int main() {
    BankAccount* bankAccount = new BankAccount(100);
    bankAccount->withdraw(100);
}
  • This changes the permitted state transitions: withdrawal used to reduce balance, now it does nothing or throws error.
  • This breaks history constraints — violating LSP.

3️⃣ Method Rule

When a method is overridden in a subclass, the behavior must not violate what the client of the base class expects.

Rule 1: Precondition Rule:

A precondition is a requirement that must be true before a method is executed.

The Precondition Rule in LSP ensures that:

A subclass must not strengthen the preconditions of a method in the superclass.

🔥 Precondition Rule in Simple Words:

  • A subclass should accept at least everything the superclass accepted.
  • It must not demand more than the base class.
  • If the base class method accepts all inputs, the subclass cannot restrict input types, ranges, or conditions.

🚫 Violation Example (C++)

class Printer {
public:
    virtual void print(const string& content) {
        cout << "Printing: " << content << endl;
    }
};

class SecurePrinter : public Printer {
public:
    void print(const string& content) override {
        if (content.find("confidential") == string::npos) {
            throw runtime_error("Only confidential documents allowed"); // ❌ Stricter condition
        }
        cout << "Secure Printing: " << content << endl;
    }
};

❌ What's wrong?

  • The base class allowed printing any content.
  • The subclass (SecurePrinter) requires “confidential” to be in the content. This is a stricter precondition, and breaks LSP.

✅ Valid Subclass: Looser or Equal Preconditions

class RelaxedPrinter : public Printer {
public:
    void print(const string& content) override {
        // Accept all content (same precondition)
        // Or, even allow empty strings (looser precondition)
        cout << "Relaxed Printing: " << content << endl;
    }
};

🧠 Why is Precondition Rule Important?

  • If a subclass expects more from the caller than the base class, then:
  • Anywhere you use the base class, replacing it with the subclass will fail.
  • This violates the Liskov Substitution Principle.

Complete Example

#include <iostream>

using namespace std;

// A Precondition must be statisfied before a method can be executed.
// Sub classes can weaken the precondition but cannot strengthen it.

class User {
public:
    // Precondition: Password must be at least 8 characters long
    virtual void setPassword(string password) {
        if (password.length() < 8) {
            throw invalid_argument("Password must be at least 8 characters long!");
        }
        cout << "Password set successfully" << endl;
    }
};

class AdminUser : public User {
public:
    // Precondition: Password must be at least 6 characters
    void setPassword(string password) override {
        if (password.length() < 6) { 
            throw invalid_argument("Password must be at least 6 characters long!");
        }
        cout << "Password set successfully" << endl;
    }
};

int main() {
    User* user = new AdminUser();
    user->setPassword("Admin1");  // Works fine: AdminUser allows shorter passwords

    return 0;
}

Rule 2: Post Condition

It says:

A subclass must not weaken the post conditions of an overridden method.

It can maintain or strengthen them.

🧩 What is a Postcondition?

A postcondition is something that must be true after a method finishes executing.

Think of it as:

“The method promises to leave the object (or return value) in a certain valid state.”

🚫 Violation Example (C++)

Let’s say the base class guarantees a positive result. The subclass must also meet that promise.

class Calculator {
public:
    // Postcondition: Returns a non-negative value
    virtual int square(int x) {
        return x * x;
    }
};

class BrokenCalculator : public Calculator {
public:
    // Violates postcondition by returning a wrong/negative result
    int square(int x) override {
        return -1 * (x * x); // ❌ returns a negative value
    }
};

❌ What's wrong?

  • Base class guarantees non-negative output.
  • Subclass returns negative value → Postcondition broken → violates LSP.

✅ Valid Subclass Example

class AccurateCalculator : public Calculator {
public:
    int square(int x) override {
        int result = x * x;
        if (x > 1000) result += 1; // strengthen postcondition
        return result; // ✅ still non-negative
    }
};
  • Still returns non-negative (respects base postcondition)
  • Adds extra behavior (e.g. minor bonus if input is large) → OK
  • This is a strengthened postcondition (allowed)

Complete Example:

#include <iostream>

using namespace std;

// A Postcondition must be statisfied after a method is executed.
// Sub classes can strengthen the Postcondition but cannot weaken it.

class Car {
protected:
    int speed;    

public:
    Car() {
        speed = 0;
    }
    
    void accelerate() {
        cout << "Accelerating" << endl;
        speed += 20;
    }

    //PostCondition : Speed must reduce after brake
    virtual void brake() {
        cout << "Applying brakes" << endl;
        speed -= 20;
    }
};

// Subclass can strengthen postcondition - Does not violate LSP
class HybridCar : public Car {
private:
    int charge;

public:

    HybridCar() : Car() {
        charge = 0;
    }

    // PostCondition : Speed must reduce after brake
    // PostCondition : Charge must increase.
    void brake() {
        cout << "Applying brakes" << endl;
        speed -= 20;
        charge += 10;
    }
};


int main() {
    Car* hybridCar = new HybridCar();
    hybridCar->brake();  // Works fine: HybridCar reduces speed and also increases charge.

    //Client feels no difference in substituting Hybrid car in place of Car.

    return 0;
}

Key Takeaways for LSP

1. 🔍 Check Behavior, Not Just Code

✅ “Just because a subclass compiles without errors doesn’t mean it obeys LSP.”

  • You must verify that subclass behavior matches the expectations set by the parent class.
  • Focus on runtime behavior, method outcomes, and state consistency.
  • Violations are often semantic, not syntactic — they won’t always show up as compiler errors.
  • Just because a child class doesn't give errors and the code compile doesn't mean it follows LSP.

2. 📋 Use Rules as a Checklist

RuleWhat it means
Method Signature RuleSame or more general argument types, compatible return types
Precondition RuleSubclass cannot strengthen the input requirements
Postcondition RuleSubclass cannot weaken the guarantees/output
Exception RuleSubclass should not throw new or unexpected exceptions
Invariant RuleSubclass must preserve or strengthen class invariants
History RuleSubclass must not alter the state mutation rules of base

3. 🛠️ How to Spot and Stop LSP Violations

✅ “When LSP is broken, expect subtle bugs, wrong results, or invalid states.”

Common signs that LSP is violated:

SymptomExample
❗ Unexpected exceptionsSubclass throws errors base never would
🧮 Wrong output valuesSubclass returns data that violates base guarantees
💣 Broken class rulesLike allowing negative balance when the base forbids it
🤔 Confusing subclass behaviorSubclass behaves differently, confusing client code

To stop violations:

  • Use interfaces/abstract classes carefully
  • Write unit tests that use base types and plug in derived classes
  • Design with contracts (Design by Contract / invariants / assertions)

✅ Final Thoughts

LSP is all about trust. When you substitute a subclass for a superclass, you should be able to trust that the behavior will remain consistent.

  • "If your code has to check the type (dynamic_cast, typeid, if (isSubclass)), it probably breaks LSP."