Liskov Substitution Principle(LSP) is about substitution of one object for another without affecting the program that uses those objects.
Barbara Liskov introduced this principle and she says
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behaviour of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
Let’s understand this with an example.
Suppose we have a class MusicPlayer
which allows users to play music
public class MusicPlayer
{
public virtual void Play()
{
// play song
}
}
We have TrialMusicPlayer
which is inherited from MusicPlayer
class
public class TrialMusicPlayer : MusicPlayer
{
public override void Play()
{
if(_songsPlayedToday >= 5)
{
throw new Exception("Only 5 songs are allowed in a day.");
}
// play song
}
}
TrialMusicPlayer
class overrides Play()
method to allow playing only 5 songs a day.
We have another class PlayerWidget
.
public class PlayerWidget
{
private readonly MusicPlayer _player;
public PlayerWidget(MusicPlayer player)
{
_player = player;
}
public void PlayButtonPressed()
{
_player.Play();
}
}
This class represents one of the user interfaces that user can use to control the music player and it simply delegates user requests to the MusicPlayer
.
The essence of Liskov Substitution Principle is that we should be able to use objects of derived type in place of objects of base type without altering the behaviour of the program that is using those objects, only then we can call the derived type as subtype.
In this example, TrialMusicPlayer
is not qualified to be subtype of MusicPlayer
because, we cannot substitute the object of TrialMusicPlayer
for the object of MusicPlayer
without changing (or breaking) PlayerWidget
. This is because PlayerWidget
is not built to handle the new exception thrown by TrialMusicPlayer
when more than 5 songs are played.
There are many subtle ways in which we can break substitutability of objects.
The following are the general rules to follow to avoid breaking substitutability
No new exception from any method in the subtype. However, we can throw new exception which is subtype of the exception thrown by base class. This is because the exception handler of a type will handle the exceptions of derived exception type as well.
Let’s first understand what covariance and contravariance mean.
Covariance and Contravariance come into picture when we are dealing with complex type with one or more independent type components in them. Generics is one such type so we use custom generic type ISpecialList<T>
for our example.
Let’s say we have a base class Person
. We have another class Employee
which is derived from Person
.
In general, if can assign object of Employee
to type Person
, but assigning object of Person
to type Employee
is not allowed.
Person p = new Employee(); // OK
Employee e = new Person(); // Compile time error
This is not true by default for generic types like ISpecialList<T>
. We cannot assign object of ISpecialList<Employee>
to type ISpecialList<Person>
or vice versa.
However, we can allow these assignments to work using variance.
If we allow assigning the object of ISpecialList<Employee>
to type ISpecialList<Person>
then we say ISpecialList<T>
is covariant
If we allow assigning the object of ISpecialList<Person>
to type ISpecialList<Employee>
then we say ISpecialList<T>
is contravariant
In order to avoid breaking substitutability, the method arguments must be contravariant and return type must be covariant in subtype.
In C#, you can create covariant interface using out
keyword and contravariant interface using in
keyword. For example,
ISpecialList<out T>
is covariant and ISpecialList<in T>
is contravariant.
Preconditions are any validation that we perform before executing a method and it should not be strengthened in a subtype
Postconditions are any validation that we perform after the method is executed and it should not be weakened in a subtype
Invariant refers to properties or states that are preserved when the method is executed. Any such invariants of supertype must be preserved in a subtype
Subtype should not introduce a new method that allows mutating state that is immutable in supertype