The Liskov Substitution Principle (LSP) states that child class objects should be able to replace parent class objects without compromising application integrity. What this means essentially, is that we should put an effort to create such derived class objects which can replace objects of the base class without modifying its behavior. If we don’t, our application might end up being broken.
Does this make sense to you?
To make things clear, we are going to use a simple „Sum Calculator“ example, which will help us to understand how to implement the LSP better.
To download the source code for this project, check out the Liskov Substitution Principle Project Source Code.
To read about other SOLID principles, check out our SOLID Principles page.
Initial Project
In this example, we are going to have an array of numbers and a base functionality to sum all the numbers from that array. But let’s say we need to sum just even or just odd numbers.
How would we implement that? Let’s see one way to do it:
public class SumCalculator { protected readonly int[] _numbers; public SumCalculator(int[] numbers) { _numbers = numbers; } public int Calculate() => _numbers.Sum(); }
public class EvenNumbersSumCalculator: SumCalculator { public EvenNumbersSumCalculator(int[] numbers) :base(numbers) { } public new int Calculate() => _numbers.Where(x => x % 2 == 0).Sum(); }
Now if we test this solution, whether we calculate the sum of all the numbers or the sum of just even numbers, we are going to get the correct result for sure:
class Program { static void Main(string[] args) { var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 }; SumCalculator sum = new SumCalculator(numbers); Console.WriteLine($"The sum of all the numbers: {sum.Calculate()}"); Console.WriteLine(); EvenNumbersSumCalculator evenSum = new EvenNumbersSumCalculator(numbers); Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}"); } }
The result is:
Creating a Better Solution
As we can see, this is working just fine. But what is wrong with this solution then?
Why are we trying to fix it?
Well, as we all know, if a child class inherits from a parent class, then the child class is a parent class. Having that in mind, we should be able to store a reference to an EvenNumbersSumCalculator
as a SumCalculator
variable and nothing should change. So, let’s check that out:
SumCalculator evenSum = new EvenNumbersSumCalculator(numbers); Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
As we can see, we are not getting the expected result because our variable evenSum
is of type SumCalculator
which is a higher order class (a base class). This means that the Count
method from the SumCalculator
will be executed. So, this is not right, obviously, because our child class is not behaving as a substitute for the parent class.
Luckily, the solution is quite simple. All we have to do is to implement small modifications to both of our classes:
public class SumCalculator { protected readonly int[] _numbers; public SumCalculator(int[] numbers) { _numbers = numbers; } public virtual int Calculate() => _numbers.Sum(); }
public class EvenNumbersSumCalculator: SumCalculator { public EvenNumbersSumCalculator(int[] numbers) :base(numbers) { } public override int Calculate() => _numbers.Where(x => x % 2 == 0).Sum(); }
As a result, when we start our solution, everything works as expected and the sum of even numbers is 18 again.
So, let’s explain this behavior. If we have a child object reference stored in a parent object variable and call the Calculate
method, the compiler will use the Calculate
method of the parent class. But right now because the Calculate
method is defined as „virtual“ and is overridden in the child class, that method in the child class will be used instead.
Implementing the Liskov Substitution Principle
Still, the behavior of our derived class has changed and it can’t replace the base class. So we need to upgrade this solution by introducing the Calculator
abstract class:
public abstract class Calculator { protected readonly int[] _numbers; public Calculator(int[] numbers) { _numbers = numbers; } public abstract int Calculate(); }
Then we have to change our other classes:
public class SumCalculator : Calculator { public SumCalculator(int[] numbers) :base(numbers) { } public override int Calculate() => _numbers.Sum(); }
public class EvenNumbersSumCalculator: Calculator { public EvenNumbersSumCalculator(int[] numbers) :base(numbers) { } public override int Calculate() => _numbers.Where(x => x % 2 == 0).Sum(); }
Excellent. Now we can start making calls towards these classes:
class Program { static void Main(string[] args) { var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 }; Calculator sum = new SumCalculator(numbers); Console.WriteLine($"The sum of all the numbers: {sum.Calculate()}"); Console.WriteLine(); Calculator evenSum = new EvenNumbersSumCalculator(numbers); Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}"); } }
We will again have the same result, 40 for all the numbers and 18 for the even numbers. But now, we can see that we can store any subclass reference into a base class variable and the behavior won’t change which is the goal of LSP.
What We Gain By Implementing the LSP
By implementing the LSP, we are keeping our functionality intact and still having our subclasses act as a substitute to a base class.
Also, we encourage the code reusability by implementing the LCP and having better project maintenance as well.
Conclusion
We can see that implementing the LSP is not that complicated but just the opposite. Most of us probably already implemented this principle many times in our code without knowing its name because in the object-oriented world Polymorphism is quite a big thing.
So, what are differences between LSP and Upcasting-Downcasting in C#
Thanks a lot.
I really got clarified today about LSV, not only that, I really get much interested while reading and acquiring your article.
Kindly keep going ahead with key concepts (Patterns, Microservices, SSO, Memory management in C# in future based of .NET)
Thank you for the comment and the suggestions. We already have a lot of articles about Design Patterns, and several Microservices articles.
I think what you explained is Open Close Principle(OCP) – which is open for extension but close for modification.You are using abstract method: Calculate() to prevent any modification in Calculator class. Derived classes:SumCalculator and EvenNumbersSumCalculator are used as extensions.
Please note that this is not Liskov Substitution Principle. As LSP is: If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.
Didn’t se just prove that? Object of type Calculator may be replaced with any of it’s subtypes, without any alterations.
I understand your point since this implementation is quite similar to our previous article where we explained ocp, but still the point is proven here. Of course, you can always provide an example and I will update the article if it is not correct. Bottom line is that we want to help and not confuse readers.
How about using interfaces with base classes or instead of base classes with a collection of objects needing calculation of the interface type instead of the class type using casting? Either? Both?
I don’t really understand the Liskov principle – surely when you create a class hierarchy by it’s very definition each layer of the hierarchy gets more targeted or specialised? So surely you’re never going to code
SumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
because you’ve explicitly desgned the child class’s Calculate() method to do a different job – to me this just seems like seriously bad programming.
I’ll admit I’m a bit old school, having leaned my coding in the days of the mainframe when OO didn’t even exist but I do try to adhere to these modern principles as most of them make perfect sense – except this one…. so what am I missing?
I enjoyed reading your post. It is always hard to come up with good, small, examples showing violations to SOLID principles and how to fix these.
There will always be exceptions to the solutions, consider for example if you have a method that expects a SumCalculator as argument and have a body like this:
public static void VerifySum(SumCalculator c)
{
if (c.Sum() != 40)
throw new ApplicationException($"Invalid sum: {sum}");
}
This will pass for a SumCalculator but fail for an EvenSumCalculator, hence the EvenSumCalculator still violates LSP. In order to fix this you would need to create a completely abstract base class without any default implementation of Sum.
I would suggest reading Uncle Bob’s book Agile Principles, Patterns, and Practices in C# for a good example on this topic.
Thank you for this suggestion. I am trying to make examples as simple as possible, due to easier understanding for the readers without any knowledge about this topic or any other topic at all. And I must say, I am not a big fun of Circle Square examples. All the best.
I am confused. Is this still a relevant comment or has further editing of the post rendered it invalid?
It seems to me that what Eric is suggesting is exactly what the abstract base class Calculator is doing, and that the function ‘VerifySum’ does not fail with the LSP implemented code, since the SumCalculator is a child class and will always return 40 (keeping the input numbers the same).
Hi Michail. The article was updated.
Thanks Marinko, just being extra-safe there…