SOLID Principles: Understanding the Liskov Substitution Principle

Article Banner
Author(s): Ajay Kumar
Last updated: 31 May 2025

🔍 What is the Liskov Substitution Principle (LSP)?


Imagine you have a universal remote at home. It works perfectly with your TV, but when you try it with your new soundbar, some buttons do the wrong thing—or nothing at all! Frustrating, right? In software, the Liskov Substitution Principle (LSP) is about making sure that when you swap one part (like a remote or a class) for another, everything still works as expected.

LSP is the third of the five SOLID principles. In simple terms: "Objects of a superclass should be replaceable with objects of a subclass without breaking the application." That means if you have code that works with a base class, it should work just as well with any derived class.

💡 Why is LSP Important?


Violating LSP leads to code that’s hard to maintain, test, and extend. If a subclass can’t be used in place of its parent, you’ll end up with bugs, unexpected behavior, and lots of if statements to handle exceptions. Here’s why LSP matters:

  • Predictability: You can trust that subclasses won’t break your code.
  • Reusability: Components can be swapped easily, making your codebase flexible.
  • Maintainability: Fewer surprises mean easier debugging and refactoring.

LSP helps you build robust, future-proof systems where new features don’t break old ones.

🕒 When Does LSP Matter Most?


LSP is crucial when you use inheritance or interfaces. Watch for these red flags 🚩:

  • Subclasses override methods and change expected behavior.
  • Code using a base class needs to check the actual type before calling methods.
  • Unit tests for subclasses look very different from those for the base class.

The earlier you spot these, the easier it is to fix. LSP is your guide to safe inheritance!

🛠️ How Do You Apply LSP in C#?


Let’s get practical. Suppose you have a Rectangle class and a Square class that inherits from it:


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

public class Square : Rectangle
{
    public override int Width
    {
        get => base.Width;
        set { base.Width = value; base.Height = value; }
    }
    public override int Height
    {
        get => base.Height;
        set { base.Width = value; base.Height = value; }
    }
}
        
Code Sample #1 : A classic LSP violation: Square inherits Rectangle

Here, Square tries to force both sides to be equal. But if you use a Square where a Rectangle is expected, you get weird results:


Rectangle rect = new Square();
rect.Width = 5;
rect.Height = 10;
Console.WriteLine(rect.Area()); // Output: 100, but you expected 50!
        
Code Sample #2 : Unexpected behavior when substituting Square for Rectangle

This breaks LSP. The Square class can’t be used everywhere a Rectangle is expected without causing bugs.

How to fix it? Prefer composition over inheritance when the relationship isn’t truly "is-a". Here’s a better approach:


public interface IShape
{
    int Area();
}

public class Rectangle : IShape
{
    public int Width { get; set; }
    public int Height { get; set; }
    public int Area() => Width * Height;
}

public class Square : IShape
{
    public int Side { get; set; }
    public int Area() => Side * Side;
}
        
Code Sample #3 : LSP-compliant design: separate classes

💡 Is inheritance always bad?

Not at all! Inheritance is powerful when used for true "is-a" relationships. LSP helps you decide when inheritance makes sense. If a subclass can’t stand in for its parent without surprises, consider using interfaces or composition instead.

Now, Rectangle and Square both implement IShape, and you can use them interchangeably without breaking expectations.

🌍 LSP in the Real World: Practical Scenarios


LSP isn’t just for shapes! It applies to services, controllers, and any place you use inheritance or interfaces. Here’s a real-world example with a payment system:


public interface IPaymentProcessor
{
    void Process(decimal amount);
}

public class CreditCardProcessor : IPaymentProcessor
{
    public void Process(decimal amount)
    {
        // Credit card processing logic
    }
}

public class PaypalProcessor : IPaymentProcessor
{
    public void Process(decimal amount)
    {
        // PayPal processing logic
    }
}
        
Code Sample #4 : LSP in services: substitutable payment processors

Any code that expects an IPaymentProcessor can use either CreditCardProcessor or PaypalProcessor without worrying about surprises. That’s LSP in action!

🚩 Common Pitfalls (and How to Dodge Them)


  • Forcing inheritance: Don’t use inheritance just to reuse code. Only use it for true "is-a" relationships.
  • Overriding with surprises: Subclasses that override methods and change expected behavior break LSP.
  • Throwing NotImplementedException: If a subclass can’t implement a method, it probably shouldn’t inherit from the base class.
  • Type checks: If you need if (obj is SubType) in your code, LSP is likely being violated.

Pro tip: When in doubt, ask: “If I swap this class for its parent, will my code still work?” If not, rethink your design!

🔗 LSP and the Other SOLID Principles


LSP works hand-in-hand with the other SOLID principles:

  • Single Responsibility Principle: LSP is easier to follow when classes have clear, focused responsibilities.
  • Open/Closed Principle: LSP enables safe extension of code without modification.
  • Interface Segregation Principle: LSP encourages small, focused interfaces that are easy to implement correctly.
  • Dependency Inversion Principle: LSP makes it safe to depend on abstractions, not concrete classes.

Mastering LSP helps you build flexible, reliable, and maintainable systems.

📝 Wrapping Up: Why LSP Makes Life Easier


The Liskov Substitution Principle is about trust and predictability. Like a universal remote that works with every device, LSP helps you build systems where parts can be swapped without fear. Use LSP to guide your inheritance and interface design, and your codebase will be easier to test, extend, and maintain.

Copyright © 2025 Dev Codex

An unhandled error has occurred. Reload 🗙