Liskov Substitution Principle (LSP)
The Liskov substitution principle (LSP) was named after Barbara Liskov, the computer scientist who defined it.
Liskov introduced the Liskov substitution principle in her conference keynote talk, “Data Abstraction,” in 1987. A few years later, she published a paper with Jeannette Wing in which they defined the principle as the following:
Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.
That definition is a bit too scientific. What does it mean?
The definition means that child class objects should be able to replace parent class objects without breaking the integrity of the application.
Any code calling methods on objects of a specific type should continue to work when those objects get replaced with instances of a subtype.
What’s a subtype?
A subtype can be either a class extending another class or a class implementing an interface.
Let’s see this in action. First, let’s look at a simple calculator example:
public class SumCalculator
{
protected readonly int[] _numbers;
public SumCalculator(int[] numbers)
{
_numbers = numbers;
}
public int Calculate() => _numbers.Sum();
}
Above is a SumCalculator
class that, when instantiated, requires an array of integers. When Calculate
is invoked, it will return the sum of all the integers in the array.
Next, inherit the SumCalculator
class which acts as a subtype for the EvenNumbersSumCalculator
class:
public class EvenNumbersSumCalculator: SumCalculator
{
public EvenNumbersSumCalculator(int[] numbers)
:base(numbers)
{
}
public new int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
}
Now, let’s utilize both classes in the console application:
public class Program
{
static void Main(string[] args)
{
int[] 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()}”);
EvenNumbersSumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
}
}
The result is:
The sum of all the numbers: 40.
The sum of all the even numbers: 18.
Great! That’s all correct!
If that’s correct, then what’s wrong?
With the LSP, a child class should be able to replace its parent class. We should be able to store a reference to EvenNumbersSumCalculator
as a SumCalculator
and nothing should change.
Let’s change the evenSum
variable within the Main
method from above to the following:
SumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Now, the output is:
The sum of all the even numbers: 40.
What? 🤔
The reason it returned 40 instead of 18 is the variable evenSum
is of type SumCalculator
, which is acting as a base class. It means that the Calculate
method from SumCalculator
will be executed instead.
It is incorrect because the child class, EvenNumbersSumCalculator
, is not acting as a substitute for its parent class, SumCalculator
.
Fix that by first implementing a small modification to both classes using the virtual
and override
modifiers. Remember, the virtual
modifier marks a method, property, indexer, or event overridable, so a derived class can use the override
modifier.
In the SumCalculator
class, change the Calculate
to this:
public virtual int Calculate() => _numbers.Sum();
And then override it in the Calculate
method in the EvenNumbersSumCalculator
class:
public override int Calculate() => _numbers.Where(x => x % 2 == 0).Sum();
The Calculate
method in the child class now overrides the parent class. So, when you rerun the program using this line:
SumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
It will return 18 instead of 40 because the Calculate
method inside EvenNumbersSumCalculator
is overriding the parent, SumCalculator
’s calculate.
Great, we’re done, right?
Well, no. Unfortunately, the behavior of the derived class, EvenNumbersSumCalculator
, has changed, and it can’t replace the base class, SumCalculator
.
In the last chapter, you learned about contracts, which are abstract classes and interfaces. I used an interface for an example of the open/closed principle. For this example, I am going to use an abstract class to create a better base class in which abstract classes are normally used.
public abstract class Calculator
{
protected readonly int[] _numbers;
public Calculator(int[] numbers)
{
_numbers = numbers
}
public abstract int Calculate();
}
Hey, doesn't that look like the SumCalculator
class?
Yes, because I was treating the SumCalculator
class as a base class. But, I changed the Calculate()
method by adding the abstract modifier, which is overridable. Remember, in the last chapter, I said that an abstract class has to have at least one abstract method.
So, now let’s have the SumCalculator
and the EvenNumbersSumCalculator
inherit the new base class, Calculator
:
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();
}
Update the console application:
public class Program
{
static void Main(string[] args)
{
int[] 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()}");
Calculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");
}
}
The result is the same:
The sum of all the numbers: 40.
The sum of all the even numbers: 18.
As you can see, you can store any subclass reference (SumCalculator
, EvenNumbersSumCalculator
) into a base class variable (Calculator sum
, Calculator evenSum
) and the behavior won’t change.
The functionality is still intact, and the subclasses continue to act as a substitute to a base class.
Let’s Recap!
Child class objects should be able to replace parent class objects.
Subtypes are either a class extending another class, like an abstract class, or a class implementing an interface.
A common use of an abstract class is as a base class.
Calling methods on objects of a specific type should continue to work when those objects get replaced with instances of a subtype.
Now that we’ve covered the “L” principle let’s move on to “I,” interface segregation.