DEV Community

Gabrielle Eduarda
Gabrielle Eduarda

Posted on

Liskov Substitution Principle: Inheritance That Looks Right But Breaks Everything

"If you need to write if (object is Subclass) at runtime, your inheritance might already be wrong."

The Liskov Substitution Principle (LSP) is often seen as academic — until it quietly breaks your system in production.

You extend a class thinking you're reusing code. But without realizing, you're creating a subclass that violates the expected behavior of the base class.

It compiles. It runs. But it doesn’t behave.

That’s what the Liskov Substitution Principle, the “L” in SOLID, helps us prevent.

What does LSP actually say?
“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.”
— Barbara Liskov (1987)

In simple terms:

A subclass must preserve the behavior expected from the base class

You should be able to use it without knowing it’s a subclass

If you need to adjust your logic to account for the subclass… it's broken

A simple analogy: plastic cup vs glass cup
Imagine a machine that fills cups with water.

If you replace a plastic cup with a glass one, everything should still work the same.
If the glass shatters because the system wasn’t expecting something so fragile… that’s a substitution violation.

Same goes for code: changing the internal behavior silently breaks trust in the abstraction.

A classic example: Rectangle vs Square
It sounds harmless.
After all, a square is a rectangle with equal sides, right?

public class Rectangle {
public virtual double Width { get; set; }
public virtual double Height { get; set; }

public double Area() => Width * Height;
Enter fullscreen mode Exit fullscreen mode

}
Now we extend it:

public class Square : Rectangle {
public override double Width {
set {
base.Width = value;
base.Height = value;
}
}

public override double Height {
    set {
        base.Width = value;
        base.Height = value;
    }
}
Enter fullscreen mode Exit fullscreen mode

}
Looks reasonable — but then:

Rectangle r = new Square();
r.Width = 5;
r.Height = 10;
Console.WriteLine(r.Area()); // Expected: 50 — Actual: 100 ❌

Boom. The behavior is broken because Square overrides properties in a way that breaks the base class’s assumptions.

How to tell you’re violating LSP
You need to check if (object is SubType) in your logic

Your subclass throws exceptions in methods expected to be safe

Your subclass changes the effect of base class methods

You can’t confidently substitute the base with the subclass

If you can’t trust the object without checking its internals, you’ve violated LSP.

The fix: Prefer composition over inheritance
Inheritance enforces contracts. Composition enables flexibility.

Let’s rewrite Rectangle and Square using interfaces:

public interface IShape {
double Area();
}

public class Rectangle : IShape {
public double Width { get; set; }
public double Height { get; set; }

public double Area() => Width * Height;
Enter fullscreen mode Exit fullscreen mode

}

public class Square : IShape {
public double Side { get; set; }

public double Area() => Side * Side;
Enter fullscreen mode Exit fullscreen mode

}
Now, each shape defines its own logic.
No forced behavior. No violations. No surprises.
**
Practical tips to apply LSP in your code**
Use interfaces to define clear expectations

Favor composition when behavior significantly differs

Write tests that swap subtypes for base types

Ask yourself: “If I replace this object with another of the same interface, will it still work as expected?”

A real-world case: payment validation

public class Payment {
public virtual void Validate() {
// basic checks
}
}

public class InternationalPayment : Payment {
public override void Validate() {
base.Validate();
CheckExchangeRates(); // might throw
}
}

If the system expects a Payment, and you pass InternationalPayment, unexpected side effects might occur — breaking behavior that should be consistent.

-Better: separate validators using interfaces:

public interface IPaymentValidator {
void Validate();
}
Each class handles its own logic — without corrupting a shared inheritance chain.

Conclusion: LSP is about trust in your architecture
Liskov Substitution Principle protects your system from fake inheritance — when things look similar but behave completely differently.

Ignoring LSP doesn’t throw compile errors.
It doesn’t warn you at runtime.
It just silently erodes your architecture until refactoring becomes a nightmare.

✔ If a subclass cannot safely replace the base class, it should not inherit from it.

Top comments (0)