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.