Understanding the Liskov Substitution Principle Through Real Inheritance Issues

Learn how improper class hierarchies can silently break your application

Posted by Hüseyin Sekmenoğlu on August 19, 2018 Object-Oriented Programming (OOP)

The Liskov Substitution Principle (LSP) is the third letter in the SOLID acronym and was introduced by computer scientist Barbara Liskov. It states that objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program.

In other words, if a piece of code uses a base class it should work with any of its subclasses without needing to know the difference. When this principle is violated, runtime errors and unexpected behavior occur.

📚 The Problem With Misused Inheritance

Inheritance is a powerful tool that allows developers to reuse common logic in child classes. However, problems arise when a parent class contains methods that do not apply to all subclasses.

Imagine an online book service where users can sign up for Standard or Premium accounts:

  • Standard users can read a limited number of books

  • Premium users can read unlimited books and add family members

It seems reasonable to define a common BaseUser class to capture shared behavior.

public class BaseUser {
    public string FullName { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }

    public virtual void AccessBook(Book book) {
        // shared access logic
    }

    public virtual void AddFamilyMember(User familyMember) {
        // premium-only logic
    }
}

Both StandardUser and PremiumUser inherit from BaseUser:

public class StandardUser : BaseUser {
    // inherits methods including AddFamilyMember
}

public class PremiumUser : BaseUser {
    public override void AddFamilyMember(User familyMember) {
        // actual implementation
    }
}

⚠️ What Can Go Wrong

Suppose someone calls AddFamilyMember() on a StandardUser instance. Since StandardUser inherits this method but should not implement it, one of two things might happen:

  • The method throws an exception

  • The method silently fails or behaves incorrectly

Either case violates the Liskov Substitution Principle. Code using the BaseUser class cannot safely treat all child types the same way.

🧹 Solution 1: Remove Inappropriate Methods From Base Class

Refactor the BaseUser class to only contain behavior that is common to all users.

public abstract class BaseUser {
    public string FullName { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }

    public abstract void AccessBook(Book book);
}

Now each subclass handles its own unique behaviour:

public class StandardUser : BaseUser {
    public override void AccessBook(Book book) {
        // limited access logic
    }
}

public class PremiumUser : BaseUser {
    public override void AccessBook(Book book) {
        // unlimited access logic
    }

    public void AddFamilyMember(User member) {
        // premium-only logic
    }
}

This avoids exceptions and keeps the model true to business rules.

💡 Solution 2: Use Interfaces to Add Optional Behaviour

Another approach is to separate optional capabilities into interfaces. For example:

public interface IFamilySharer {
    void AddFamilyMember(User member);
}

Then only PremiumUser implements this interface:

public class PremiumUser : BaseUser, IFamilySharer {
    public override void AccessBook(Book book) {
        // unlimited access
    }

    public void AddFamilyMember(User member) {
        // share access
    }
}

Now, code that needs family sharing can check for the interface:

if (user is IFamilySharer sharer) {
    sharer.AddFamilyMember(familyUser);
}

This approach allows maximum flexibility while preserving correctness.

✅ Summary

The Liskov Substitution Principle ensures that inheritance hierarchies remain safe and logical. Following LSP helps you:

  • Avoid runtime exceptions

  • Model real-world logic more accurately

  • Design flexible systems that grow safely

If you find yourself writing throw NotImplementedException() inside a derived class you are likely violating LSP. Use composition or interfaces instead to protect your application from silent failures.