Understanding the Liskov Substitution Principle (LSP) with C#
In our ongoing series on SOLID principles, we've already covered the Single Responsibility Principle (SRP) and the Open/Closed Principle (OCP). In this post, we'll dive into the Liskov Substitution Principle (LSP), the third principle in the SOLID acronym.
Liskov Substitution Principle
The Liskov Substitution Principle (LSP) was introduced by Barbara Liskov in 1987. The principle states that objects of a base class should be replaceable with objects of a derived class without affecting the correctness of the program.
In simpler terms, if a class B
is a derived class of class A
, then instances of A
should be replaceable with instances of B
without altering the desirable properties of the program.
This principle helps us design a system where derived classes extend the functionality of the base class without changing the expected behavior from the client’s perspective. If a derived class modifies the behavior of a base class in such a way that it violates the expectations set by the base class, then it’s not adhering to LSP.
Example Scenario
Let’s look at a real-world example to understand LSP in the context of a Library Management System.
Initial Design
Imagine we have a simple class hierarchy to manage library items. We start with a base class LibraryItem
and have two derived classes: Book
and ReferenceBook
.
public class LibraryItem
{
public string Title { get; set; }
public string Author { get; set; }
public virtual void BorrowItem()
{
Console.WriteLine($"{Title} borrowed.");
}
public virtual void ReturnItem()
{
Console.WriteLine($"{Title} returned.");
}
}
public class Book : LibraryItem
{
public override void BorrowItem()
{
base.BorrowItem();
Console.WriteLine($"{Title} is due in 14 days.");
}
}
public class ReferenceBook : LibraryItem
{
public override void BorrowItem()
{
throw new InvalidOperationException("Reference books cannot be borrowed.");
}
public override void ReturnItem()
{
throw new InvalidOperationException("Reference books cannot be borrowed or returned.");
}
}
In the above design:
LibraryItem
is the base class with common properties likeTitle
andAuthor
.Book
is a regular library item that can be borrowed and returned. This is a derived class.ReferenceBook
, however, is a type ofLibraryItem
that cannot be borrowed or returned. This is also a derived class.
Violating LSP
Now, consider the following code that uses these classes:
public void ProcessBorrowing(LibraryItem item)
{
item.BorrowItem();
}
If we pass an instance of Book
to ProcessBorrowing
method, it works as expected. However, if we pass an instance of ReferenceBook
, it throws an exception. This violates LSP because ReferenceBook
does not behave as a proper derived class of LibraryItem
as it changes the expected behavior of BorrowItem
method.
Refactoring to Adhere to LSP
To adhere to LSP, we need to rethink our design. Since not all LibraryItem
s can be borrowed, we should separate the borrowable items from non-borrowable ones. This can be achieved by introducing an interface:
public interface IBorrowable
{
void BorrowItem();
void ReturnItem();
}
public class LibraryItem
{
public string Title { get; set; }
public string Author { get; set; }
}
public class Book : LibraryItem, IBorrowable
{
public void BorrowItem()
{
Console.WriteLine($"{Title} borrowed.");
Console.WriteLine($"{Title} is due in 14 days.");
}
public void ReturnItem()
{
Console.WriteLine($"{Title} returned.");
}
}
public class ReferenceBook : LibraryItem
{
// No implementation of IBorrowable, as reference books can't be borrowed.
}
Now, our LibraryItem
class is a general class that doesn't impose borrowing behavior. The Book
class implements the IBorrowable
interface, making it clear that Book
can be borrowed. The ReferenceBook
class doesn't implement IBorrowable
, so it doesn't participate in borrowing operations.
The ProcessBorrowing
method can now be refactored to handle only borrowable items:
public void ProcessBorrowing(IBorrowable item)
{
item.BorrowItem();
}
This way, only objects that implement IBorrowable
will be passed to ProcessBorrowing
, adhering to LSP.
Benefits of Adhering to LSP
By adhering to LSP, our code becomes more reliable and easier to maintain. We avoid unexpected exceptions and ensure that our derived class implementations are consistent with the expectations set by the base class.
Here are some key benefits:
Predictable Behavior: Substituting one class for another does not introduce unexpected behavior or errors.
Extensibility: Adding new types of borrowable or non-borrowable items becomes straightforward.
Maintenance: The code is easier to understand and maintain since the responsibilities and behaviors of classes are well-defined.
Summary
The Liskov Substitution Principle ensures that a class hierarchy remains consistent and predictable, which is essential for building scalable and maintainable systems. In our Library Management System example, refactoring our design to adhere to LSP resulted in a cleaner and more robust implementation.
Subscribe to my newsletter
Read articles from Geo J Thachankary directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by