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
- Behavioral Compatibility: A subclass must exhibit the behavior expected by the parent class. It should not introduce unexpected behavior.
- Contract Preservation: Subclasses must adhere to the "contract" defined by the parent class (method definitions, preconditions, postconditions, invariants).
- 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:
- 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. - Substitution Breaks: Replacing
Rectangle
withSquare
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:
- No Inheritance Conflicts:
Rectangle
andSquare
are independent implementations of theShape
interface. - 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:
Rule | LSP Status | Notes |
---|---|---|
Subclass uses same parameter type | ✅ Valid | Proper overriding |
Subclass uses a narrower type | ❌ Invalid | Not substitutable |
Subclass uses a wider type (in theory) | ✅ Conceptually valid | But not supported in C++ overriding |
Subclass changes number of parameters | ❌ Invalid | Breaks 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 ofAnimal*
, 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 usingDog
.
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
Aspect | Rule | Valid 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 ofno 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 handlestd::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
Principle | Explanation |
---|---|
No new exceptions | Subclass should not introduce new exception types |
No broader exceptions | Don't replace specific with general (e.g., Exception) |
Can throw fewer exceptions | Subclass may choose to throw fewer or none |
Respect exception guarantees | If 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 allowsbalance < 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 aCheatAccount
.
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 whenmove()
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
Rule | What it means |
---|---|
Method Signature Rule | Same or more general argument types, compatible return types |
Precondition Rule | Subclass cannot strengthen the input requirements |
Postcondition Rule | Subclass cannot weaken the guarantees/output |
Exception Rule | Subclass should not throw new or unexpected exceptions |
Invariant Rule | Subclass must preserve or strengthen class invariants |
History Rule | Subclass 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:
Symptom | Example |
---|---|
❗ Unexpected exceptions | Subclass throws errors base never would |
🧮 Wrong output values | Subclass returns data that violates base guarantees |
💣 Broken class rules | Like allowing negative balance when the base forbids it |
🤔 Confusing subclass behavior | Subclass 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."