principios-solid-lsp-sustitucion-liskov

Liskov Substitution Principle (LSP) in SOLID

  • 4 min

The Liskov Substitution Principle requires that a child class can substitute its parent class without breaking the program.

We’ve reached the midpoint of SOLID with the letter L: the Liskov Substitution Principle (LSP).

This principle is named after Barbara Liskov, a Turing Award winner. It’s based on an idea she proposed in 1987 and later formalized together with Jeannette Wing.

Its formal definition is a bit intimidating:

“If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.”

Phew! What does this mean in plain English? Basically, if one class inherits from another, the child class must respect the expected behavior when used in place of the parent class.

The Classic Problem: The Square and the Rectangle

To understand this, there’s no better example than the famous square and rectangle problem. It’s the canonical example because it demonstrates how mathematical logic and object logic don’t always align.

In geometry, a square is a rectangle (a special case where all sides are equal). So our intuition tells us: “Great, I’ll make Square inherit from Rectangle.

The Parent Class: Rectangle

We have a standard rectangle. We assume we can change the width and height independently.

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

    public int CalculateArea() => Width * Height;
}
Copied!

The Child Class: Square

Since a square must have equal sides, we override the setters to enforce this rule.

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

    public override int Height
    {
        set { base.Width = value; base.Height = value; }
    }
}
Copied!

The Test That Fails

This is where the LSP breaks down. Suppose we have a function that receives a Rectangle (the parent class) and runs a test.

public void TestArea(Rectangle r)
{
    r.Width = 5;
    r.Height = 4;

    // If it's a rectangle, the area should be 5 * 4 = 20.
    if (r.CalculateArea() != 20)
        throw new Exception("Broken math!");
}
Copied!

If we pass a Square object to this function (which expects a Rectangle), the result is not what we expect:

  1. r.Width = 5 → The square becomes 5×5.
  2. r.Height = 4 → The square becomes 4×4 (because setting the height also changes the width).
  3. CalculateArea() returns 16.
  4. The test fails! The program expected 20.

Conclusion: Square is not substitutable for Rectangle. We have violated the LSP.

Another Symptom: Unexpected Exceptions

Another very common case of LSP violation is when we force an interface and “disable” parts that don’t apply.

Suppose we have a base class Bird with a method Fly(). We create the class Penguin that inherits from Bird. Since penguins don’t fly, we do this:

public class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotImplementedException("Penguins don't fly");
    }
}
Copied!

If you have code that iterates through a list of Bird and calls Fly() on each one, the program will crash when it reaches the penguin.

If you have to throw an exception, leave a method empty, or check the type with if (obj is Penguin) to avoid calling a method, you are violating the LSP.

The Rules of the Contract

LSP is based on “Design by Contract.” When you inherit, you accept a contract.

  1. Preconditions: You cannot be more restrictive than your parent. (If your parent accepts any number, you cannot accept only positive ones).
  2. Postconditions: You cannot offer less than your parent. (If your parent promises to return a valid object, you cannot return null).
  3. Invariants: You must maintain the rules that the parent assumes are always true (like in the rectangle case, where height and width are assumed to be independent).

How to Fix It?

Generally, LSP violations indicate that our inheritance hierarchy is poorly designed.

In the bird case:

  • Not all birds fly. Perhaps Fly() shouldn’t be in Bird.
  • We could have Bird and then FlyingBird and NonFlyingBird.

In the rectangle case:

  • A Square and a Rectangle are shapes, but they don’t share modification behavior.
  • They could implement a common interface IShape that has CalculateArea(), but one should not inherit from the other.

Always ask yourself: Is it a…? (Is-a). Not in the real-world sense, but in the sense of behavior within your program.