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.