L – Liskov Substitution Principle (LSP) | Master CPP Design Patterns 2025

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle is one of the SOLID principles of object-oriented programming. It was introduced by Barbara Liskov in 1987 and it states:

“Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.”

What It Really Means:

Imagine you’re working with a class called Bird that has a method fly(). If you create a subclass Penguin from Bird, but Penguin can’t fly, then substituting Bird with Penguin could break your program. That’s a violation of LSP.

The principle pushes us to design subclasses that truly behave like their parent class, so code that uses the base class doesn’t have to worry about the specific subclass being used.

LSP in Action (Simple Example):

class Bird {
public:
    virtual void fly() {
        std::cout << "Flying..." << std::endl;
    }
};

class Sparrow : public Bird {
public:
    void fly() override {
        std::cout << "Sparrow flying!" << std::endl;
    }
};

// This class violates LSP
class Ostrich : public Bird {
public:
    void fly() override {
        throw std::logic_error("Ostriches can't fly!");
    }
};

In the example above, Ostrich breaks LSP because it can’t perform the fly() behavior expected from Bird.

Why It Matters:

  • Encourages safe inheritance.
  • Makes code more reliable and maintainable.
  • Helps avoid unexpected runtime errors.
  • Ensures that your class hierarchy makes logical sense.

How to Stick to LSP:

  • Don’t override methods in a way that changes their expected behavior.
  • Use composition over inheritance if subclass behavior doesn’t match the base class.
  • Clearly define contracts (what methods are supposed to do) and ensure subclasses respect them.

What is LSP?

Definition by Barbara Liskov:

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.

Simple Version:

A subclass should be able to stand in for its parent class without breaking the program.

What does that mean?

Let’s break it down:

  • You create a base class (Parent class).
  • Then you make a derived class (Child class).
  • The child should behave in a way that’s expected from the parent.
  • You should be able to use the child wherever you used the parent, and things should just work.

Real-Life Analogy

Let’s say:

  • You have a class “Bird” with a method fly().
  • You create a subclass “Parrot” — fine, it can fly.
  • Now you create a subclass “Penguin” — 🐧 uh-oh! Penguins can’t fly.

If you call fly() on a Penguin object assuming it’s a Bird, your program breaks the expectation.

🚫 That violates LSP!

Example in C++ that Violates LSP

class Bird {
public:
    virtual void fly() {
        cout << "Flying..." << endl;
    }
};

class Sparrow : public Bird {
public:
    void fly() override {
        cout << "Sparrow flying" << endl;
    }
};

class Ostrich : public Bird {
public:
    void fly() override {
        throw runtime_error("Ostriches can't fly!");
    }
};

Problem:

  • If someone uses Bird* b = new Ostrich(); b->fly(); expecting it to fly — it crashes.
  • Ostrich is a Bird, but it doesn’t behave like a Bird should.

This violates LSP.

LSP-Compliant Design

Solution: Refactor your classes to respect behavior expectations.

class Bird {
public:
    virtual void eat() {
        cout << "Bird eating" << endl;
    }
};

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

class Sparrow : public FlyingBird {
public:
    void fly() override {
        cout << "Sparrow flying" << endl;
    }
};

class Ostrich : public Bird {
    // No fly() here
};

Now:

  • Ostrich doesn’t pretend to be a flying bird.
  • If your logic requires only flying birds, you use the FlyingBird type.
  • LSP is safely respected.

LSP in Code: A Simple, Clear Example

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

protected:
    int width;
    int height;
};

Now we make a Square from Rectangle:

class Square : public Rectangle {
public:
    void setWidth(int w) override {
        width = height = w;
    }

    void setHeight(int h) override {
        width = height = h;
    }
};

Problem:

If someone does this:

Rectangle* r = new Square();
r->setWidth(5);
r->setHeight(10);
cout << r->getArea();  // Expected: 50, But it gives 100

Uh-oh! Because setting width or height sets both in Square.
This breaks expected behavior.
It violates LSP.

LSP-Compliant Refactor

Instead, don’t inherit Square from Rectangle directly if they behave differently.

Maybe use composition or separate hierarchy.

LSP Summary Table

ConceptBad (Violates LSP)Good (Follows LSP)
Penguin inherits Bird with fly()❌ Penguins can’t fly✅ Separate class for non-flying birds
Square inherits Rectangle❌ Area breaks expectations✅ Design Square separately
Subclass breaks parent’s rule❌ Throws, skips behavior✅ Behaves as parent promises

📦 Key Tips to Follow LSP

  • ✅ Derived classes must honor contracts/behavior of base class.
  • ❌ Don’t override methods in a way that changes expected behavior.
  • ✅ Use interfaces or abstract classes to separate capabilities (like flying).
  • ✅ Prefer composition over inheritance when behaviors differ.

Want to Try a LSP-based Mini Project?

We can build:

  • A PaymentMethod base class (pay())
  • Subclasses: CreditCard, UPI, Wallet
  • And see how breaking or respecting LSP affects real logic

You can also Visit other tutorials of Embedded Prep 

Special thanks to @mr-raj for contributing to this article on Embedded Prep

Leave a Reply

Your email address will not be published. Required fields are marked *