DEV Community

Cover image for Who’s Afraid of the Liskov Substitution Principle?
Mark Jack
Mark Jack

Posted on

Who’s Afraid of the Liskov Substitution Principle?

Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.

Okay... this could be scary. Let's make it simpler.

The Liskov Substitution Principle (LSP) is one of the five SOLID principles of object-oriented programming. Often overlooked or misunderstood, it’s actually fundamental for writing maintainable, extensible, and truly “object-oriented” code.

What is the Liskov Substitution Principle?

Formulated by Barbara Liskov in 1987, the principle states:

“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 that program (correctness, task performed, etc.).”

Put simply: a subclass should be usable anywhere its superclass is expected, without causing unexpected behaviors. If this doesn’t happen, inheritance is being misused.

Why is it important?

Violating the Liskov Substitution Principle leads to bugs that are hard to find, fragile code, and systems that are difficult to evolve. Respecting LSP, instead, lets you fully leverage the power of polymorphism.


A Bad Example in C#
Let’s try modeling rectangles and squares:

public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    public int Area()
    {
        return Width * Height;
    }
}

public class Square : Rectangle
{
    public override int Width
    {
        get { return base.Width; }
        set { base.Width = base.Height = value; }
    }
    public override int Height
    {
        get { return base.Height; }
        set { base.Width = base.Height = value; }
    }
}
Enter fullscreen mode Exit fullscreen mode

At first glance, it seems reasonable: a square is just a special kind of rectangle, right? But let’s see what happens when we use these classes:

Rectangle rect = new Square();
rect.Width = 5;
rect.Height = 10;
Console.WriteLine(rect.Area()); // What do we expect?
Enter fullscreen mode Exit fullscreen mode

You might expect 5×10=50, but the result will be 100!The Square class forces width and height to always be equal, breaking the expectations set by the Rectangle base class. This is a classic violation of the Liskov Substitution Principle.


Refactor: How to Respect the Principle

To respect the principle, avoid inheritance where it doesn’t make sense. Rectangle and Square are siblings, not parent/child. You can instead have them inherit from a common abstract base class:

public abstract class Shape
{
    public abstract int Area();
}

public class Rectangle : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }
    public override int Area()
    {
        return Width * Height;
    }
}

public class Square : Shape
{
    public int Side { get; set; }
    public override int Area()
    {
        return Side * Side;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now you can use rectangles and squares polymorphically, with no unexpected behaviors:

Shape shape1 = new Rectangle { Width = 5, Height = 10 };
Shape shape2 = new Square { Side = 5 };
Console.WriteLine(shape1.Area()); // 50
Console.WriteLine(shape2.Area()); // 25
Enter fullscreen mode Exit fullscreen mode

Ehi.. wait! But isn’t a square just a special type of rectangle?

From a mathematical perspective, yes: a square is a particular kind of rectangle (four right angles and all sides equal). So, conceptually, the relationship “Square is a Rectangle” seems to make sense.

However, in object-oriented programming (OOP), this relationship can cause practical problems. The issue isn’t mathematics, but rather behavior.

Why shouldn’t Square inherit from Rectangle in OOP?

When you create a subclass in OOP, the Liskov Substitution Principle requires that everything true for the base class must remain true for the subclass. In our example:

A Rectangle lets you set Width and Height independently.

A Square must always have Width == Height.

If you inherit, you’re forced into an awkward situation where changing Width in a Square also changes Height (and vice versa), since all sides must be equal. But this means the Square no longer behaves like a regular Rectangle!

When does inheritance make sense?

If the subclass does not alter the invariants or rules of the base class (i.e., it doesn’t change pre-conditions, post-conditions, or invariants), then yes, inheritance is fine.

Let's enhance the article with another practical example:

Bad example:

public class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("I'm flying!");
    }
}

public class Penguin : Bird
{
    // The Fly method is inherited, but it makes no sense for a penguin!
}

public class Program
{
    public static void MakeItFly(Bird bird)
    {
        bird.Fly();
    }

    public static void Main()
    {
        Bird tweety = new Bird();
        Penguin pingu = new Penguin();

        MakeItFly(tweety); // Output: I'm flying!
        MakeItFly(pingu);  // Output: I'm flying! (Semantic error)
    }
}
Enter fullscreen mode Exit fullscreen mode

Good Example:

public abstract class Bird
{
    // All common properties
}

public class FlyingBird : Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("I'm flying");
    }
}

public class Penguin : Bird
{

}

public class Sparrow : FlyingBird
{

}

public class Program
{
    public static void MakeItFly(FlyingBird bird)
    {
        bird.Fly();
    }

    public static void Main()
    {
        Sparrow sparrow = new Sparrow();
        Penguin pingu = new Penguin();

        MakeItFly(sparrow); // Output: I'm flying!
        // MakeItFly(pingu); // Compilation error! (GOOD!)
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusions

The Liskov Substitution Principle isn’t just theory: ignoring it makes code fragile and unpredictable. When modeling class hierarchies, always ask yourself: “Can I substitute the base class with the subclass without breaking anything?” If the answer is no, you’re probably heading for design trouble.

If you found this article helpful follow me on GitHub, Twitter and Bluesky for more content.

Thanks for reading!

Top comments (0)