Below is code from an online tutorial (https://code-maze.com/liskov-substitution-principle/):
// version 1
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();
}
then we can do:
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();
SumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
}
}
So we can store the child instance (new EvenNumbersSumCalculator(numbers)
) into a parent variable (SumCalculator evenSum
), so above code complies with Liskov principle, isn't it?
But the tutorial says version 1 doesn't comply with Liskov principle and we need to do:
// version 2
public abstract class Calculator
{
protected readonly int[] _numbers;
public Calculator(int[] numbers)
{
_numbers = numbers;
}
public abstract int Calculate();
}
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();
}
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()}");
}
}
I don't understand why version 1 doesn't comply with Liskov principle?
Your version 1
is incompatible with LSP, independent of your example program.
The fact that you can "store a child instance into a parent variable" is a syntactic notion of subtyping, as supported by C#. LSP offers a behavioral notion of subtyping, insisting that the meaning (the semantics) of the supertype is preserved in any subclass.
In version 1
, the superclass Calculate
method counts all numbers, but in the subclass only the even numbers. This makes the behavior of the subclass inconsistent with the superclass.
In the LSP-compliant version 2
, this is avoided by adding a separate class without any behavior, and extending it twice, in two independent subtypes. This is LSP compliant.
If you are looking for an example in which you want to keep superclass behavior, think of a class that does something extra besides the calculation, for example a LoggingCalculator
in which the Calculate
method first calls the superclass method (keeping the same behavior) and then extends it by also logging the results somewhere.