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
Concept | Bad (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
- What is eMMC (Embedded MultiMediaCard) memory ?
- Top 30+ I2C Interview Questions
- Bit Manipulation Interview Questions
- Structure and Union in c
- Little Endian vs. Big Endian: A Complete Guide
- Merge sort algorithm
Special thanks to @mr-raj for contributing to this article on Embedded Prep
Leave a Reply