The Liskov Substitution Principle states that a derived type should be completely replaceable for its base type.
In simpler terms, a child class should do at least what the parent class can do. The child class can have additional behaviors, but it must have all the behaviors that the parent class has.
For instance, an Animal has behaviors (eat, sleep, makeSound). A Dog, which is a subtype of Animal, must have all the behaviors (eat, sleep, makeSound).
In simpler terms, if you ask for an animal which eat, sleep and makeSound then you must be okay with having a dog, a cat, or a horse.
So, that is basically the LSP. LSP is all about having well-designed polymorphic classes. Letβs understand what constitutes bad polymorphic design.
Other SOLID principles
- Single Responsibility Principle
- Open Closed Principle
- Interface Segregation Principle
- Dependency Inversion Principle
A code that is violating the LSP
public abstract class Bird
{
public abstract void Eat();
public abstract void MakeSound();
public abstract void Fly();
public void Sleep() => Console.WriteLine("Sleeping..");
}
public class Sparrow : Bird
{
public override void Eat() => Console.WriteLine("Sparrow is eating..");
public override void Fly() => Console.WriteLine("Sparrow is flying..");
public override void MakeSound() => Console.WriteLine("chirp..");
}
public class Penguin : Bird
{
public override void Eat() => Console.WriteLine("Penguin is eating..");
public override void Fly() => throw new NotImplementedException();
public override void MakeSound() => Console.WriteLine("Penguin's sound");
}
public class BirdTest
{
public static void Main()
{
Bird sparrow = new Sparrow();
sparrow.Fly();
Bird penguin = new Penguin();
penguin.Fly(); // throws exception
}
}
Bird
is an abstract class that has methods like Eat(), MakeSound(), Sleep(), and Fly(). Sparrow and Penguin inherit from the Bird class. Since a Sparrow is a Bird, a sparrow must do what a bird can do (such as eat, make sound, sleep, and fly), and it does.
A Penguin is a Bird too, so a penguin must do what a bird can do (such as eat, make sound, sleep, and fly). However, a penguin cannot fly. When you call the Fly() method of the Penguin class, it throws an error. Therefore, a penguin is not completely replaceable for a bird. This code violates the Liskov Substitution Principle (LSP).
Solution with LSP
Solution 1
Letβs create the IBird
interface.
public interface IBird
{
void Eat();
void MakeSound();
void Sleep();
}
We need to have another interface which will have the birdβs behavior with flying capabilities.
public interface IFlyableBird:IBird
{
void Fly();
}
Since sparrow can fly, so it will implement the IFlyableBird
interface.
public class Sparrow : IFlyableBird
{
public void Eat() => Console.WriteLine("Sparrow is eating..");
public void Fly() => Console.WriteLine("Sparrow is flying..");
public void MakeSound() => Console.WriteLine("chirp..");
public void Sleep() => Console.WriteLine("Sleeping");
}
Penguins do not fly, so they will implement the bird interface.
public class Penguin : IBird
{
public void Eat() => Console.WriteLine("Penguin is eating..");
public void MakeSound() => Console.WriteLine("Penguin's sound");
public void Sleep() => Console.WriteLine("Sleeping");
}
Sleep()
method is common between all the birds. In our refactored code, we need to implement Sleep() method in every bird related class (eg. Penguin,Sparrow). This does not seem to be a problem now. But, what if we have multiple methods where which shares between all the classes. In that case this solution does not seem right to me.
Solution 2: Reusing the shared methods (Sleep())
First and foremost, we need to create an interface IBird
.
public interface IBird
{
void MakeSound();
void Eat();
void Sleep();
}
We need to encapsulate out the fying behaviour. So create an interface with the name IFlyable
public interface IFlyableBird : IBird
{
void Fly();
}
Since we want to reuse the some functionality of the bird like Sleep(). We will be needed an abstract class to.
public abstract class Bird
{
public abstract void Eat();
public abstract void MakeSound();
public void Sleep() => Console.WriteLine("Sleeping..");
}
Since sparrow can fly the it will inherit the Bird
class and implement the IFlyable
interface.
public class Sparrow : Bird, IFlyableBird
{
public override void Eat() => Console.WriteLine("Sparrow is eating..");
public void Fly() => Console.WriteLine("Sparrow is flying..");
public override void MakeSound() => Console.WriteLine("chirp..");
}
Since penguin does not fly so it will only inherit the Bird
class.
public class Penguin : Bird
{
public override void Eat() => Console.WriteLine("Penguin is eating..");
public override void MakeSound() => Console.WriteLine("Penguin's sound");
}
Now letβs test this functionality.
public class BirdTest
{
public static void Main()
{
Console.WriteLine("Sparrow's behaviors\n");
IFlyableBird sparrow = new Sparrow();
sparrow.Fly();
sparrow.MakeSound();
sparrow.Eat();
sparrow.Sleep();
Console.WriteLine("\nPenguine's behaviors");
Bird penguin = new Penguin();
penguin.Eat();
penguin.Sleep();
penguin.MakeSound();
}
}
Summary
A subclass should extend but not contradict the behavior of its superclass. This principle ensures that any client code expecting an instance of the superclass can also work seamlessly with instances of any subclass.
Adhering to LSP promotes robust polymorphic designs where subclasses not only extend but also maintain the expected behavior of their superclass, ensuring code flexibility and reliability in polymorphic scenarios.