Liskov Substitution
By Caroline on June 24, 2019
3 | The Liskov Substitution Principle
Barbara Liskov and Jeannette Wing described the principle in their 1994 paper, A Behavioral Notation of Subtyping as follows:
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.
So what does that even mean? Here’s a more simple definition:
Objects should be replaceable by their subtypes.
Essentially this is saying that a subtype (child) should be able to be used anywhere that a supertype (parent) is used; that the application will behave the same way. Subtypes may have slightly different implementations from their parent or supertype, and this implementation difference is an example of polymorphism. Subtypes do not need to be directly inherited from the superclass. Interestingly, as far as the principle is concerned, a subtype is anything that is interchangable and successfully interacts with a method the same way as the supertype!
Why is this important?
By adhering to the Liskov Substitution Principle, applications have the ability to expand more freely, with less intricate dependencies, and less unexpected consequences. If the LSP is not followed, any future changes to applications will likely force previously written code to be modified, (thereby causing headaches and breaking the Open/Closed Principle!) When implementing the LSP it is important to consider any conditions that are placed on the subtype. If additional parameters are required when the subtype is called/instantiated, the subtype has stricker pre-conditions, which violates LSP.
How to fix this?
The best way is to use abstraction and encapsulation as early as possible. In a Tic Tac Toe Application, consider a runner that can create different types of games - one-player (against a computer), or two-player game (two humans against each other). The runner does not need to know which type of game is being created - as far as it’s concerned, they are the same - the game play is identical, each player takes turns playing their marker until the game is won. However the game itself should know what type it is; a solution could be to have a method to query the subtype’s own state.
Let’s take a look at another example of how to implement LSP - see the code below. Let’s say I am growing my own virtual garden, and I have a few different types of vines.
namespace Garden
{
public class Vine
{
int age;
int height;
int requiredLight;
public Vine(int requiredLight)
{
this.age = 0;
this.height = 0;
this.requiredLight = requiredLight;
}
public void Grow(int light)
{
if (light >= requiredLight)
{
height += 1;
age += 1;
}
}
public void Bloom(int light)
{
if (light >= requiredLight && age >= 5)
{
Console.Write("Your vine is flowering!");
}
}
}
public class Pothos: Vine
{
public int age;
public int requiredLight;
public int height;
public Pothos(int requiredLight)
{
this.requiredLight = requiredLight;
}
}
public class Monstera: Vine
{
public int age;
public int requiredLight;
public int height;
public Monstera(int requiredLight)
{
this.requiredLight = requiredLight;
}
}
}
Here I have a parent class - Vine
and two child classes Pothos
and Monstera
that inherit directly from Vine
. The parent class has two methods Grow
and Bloom
. You would think that all vines have these capabilities and therefore it would make complete sense to provide all vines with these behaviors. But actually it turns out that Pothos plants do not have the capability to flower, and in fact the last time a pothos plant bloomed naturally was in 1962! The child class Pothos
should no longer have the Bloom
method. Here you can see that the Liskov Principle would be violated, the parent class has behaviors that the child does not! Let’s modify this to follow LSP:
namespace Garden
{
public interface IVine
{
int age {get;set;}
int requiredLight {get;set;}
int height {get;set;}
void Grow(int light);
}
}
namespace Garden
{
public class Pothos : IVine
{
public int age;
public int height;
public int requiredLight;
public Pothos(int requiredLight)
{
this.requiredLight = requiredLight;
}
public void Grow(int light)
{
if (light >= requiredLight)
{
height += 1;
age += 1;
}
}
}
}
namespace Garden
{
public class Monstera : IVine
{
public int age;
public int height;
public int requiredLight;
public Monstera(int requiredLight)
{
this.requiredLight = requiredLight;
}
public void Grow(int light)
{
if (light >= requiredLight)
{
height += 5;
age += 1;
Bloom();
}
}
private void Bloom()
{
if (age >= 5)
{
Console.Write("Your vine is flowering!");
}
}
}
}
Much better! Here I am using an interface, it is an excellent way to provide custom behaviors to the subtypes, whilst adhering to the Liskov Principle. I have created an interface IVine
. Within the interface I have defined variables that exist for all vines, along with the method Grow
. Note that in the interface the Grow
method is only defined. There are two implementations of the interface IVine
- Pothos
and Monstera
. The Pothos
has just one method - Grow
, whereas the Monstera
has two - Grow
and the private method Bloom
. Whenever the Monstera Grow
method is called, the plant has the potential to flower, as the Bloom
method is called within Grow
. This means that wherever these plants are used in my virtual garden application, whether it’s the parent/supertype or either subtype implementation - Pothos
or Monstera
, the available methods remain the same, making them completely interchangeable. This is an excellent example of polymorphism, as the subtypes have slightly different behaviors and my garden application now follows the Liskov Substitution Principle!