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
Mr. Raj Kumar is a highly experienced Technical Content Engineer with 7 years of dedicated expertise in the intricate field of embedded systems. At Embedded Prep, Raj is at the forefront of creating and curating high-quality technical content designed to educate and empower aspiring and seasoned professionals in the embedded domain.
Throughout his career, Raj has honed a unique skill set that bridges the gap between deep technical understanding and effective communication. His work encompasses a wide range of educational materials, including in-depth tutorials, practical guides, course modules, and insightful articles focused on embedded hardware and software solutions. He possesses a strong grasp of embedded architectures, microcontrollers, real-time operating systems (RTOS), firmware development, and various communication protocols relevant to the embedded industry.
Raj is adept at collaborating closely with subject matter experts, engineers, and instructional designers to ensure the accuracy, completeness, and pedagogical effectiveness of the content. His meticulous attention to detail and commitment to clarity are instrumental in transforming complex embedded concepts into easily digestible and engaging learning experiences. At Embedded Prep, he plays a crucial role in building a robust knowledge base that helps learners master the complexities of embedded technologies.
Leave a Reply