DEV Community

Cover image for Liskov Substitution Principle in C# with Bird Example
DotNet Full Stack Dev
DotNet Full Stack Dev

Posted on

Liskov Substitution Principle in C# with Bird Example

The Liskov Substitution Principle (LSP) is one of the five SOLID principles of object-oriented design.

"Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."

In simpler terms, if you have a base class (or interface) and a derived class, you should be able to replace the base class with the derived class without introducing errors or changing the expected behavior of the program.

📌Explore more at: https://dotnet-fullstack-dev.blogspot.com/
🌟 Sharing would be appreciated! 🚀

The Problematic Code

Let's start with an example that violates the LSP. Consider a base class Bird and a derived class Ostrich.

public class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("Flying");
    }
}

public class Ostrich : Bird
{
    public override void Fly()
    {
        throw new NotSupportedException("Ostriches can't fly");
    }
}

public class Program
{
    public static void Main()
    {
        Bird bird = new Ostrich();
        bird.Fly(); // Throws NotSupportedException
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example,

  • The Bird class has a method Fly that prints "Flying".
  • The Ostrich class inherits from Bird but overrides the Fly method to throw a NotSupportedException because ostriches cannot fly.

When you instantiate an Ostrich and assign it to a Bird reference, calling the Fly method results in an exception. This violates the LSP because substituting an Ostrich for a Bird changes the expected behavior, making the program incorrect.

Adhering to LSP

To adhere to LSP, we need to ensure that substituting a subclass for a superclass does not break the program. One way to achieve this is by creating a more appropriate class hierarchy or using interfaces to represent different behaviors.

Step 1. Define an Interface for Birds.

First, we define an interface IBird that represents the common behavior of all birds.

public interface IBird
{
    void Move();
}
Enter fullscreen mode Exit fullscreen mode

Step 2. Implement Specific Bird Classes.

Next, we implement specific bird classes that adhere to the IBird interface. Each class will have its own implementation of the Move method, encapsulating the specific movement behavior of the bird.

FlyingBird Class

public class FlyingBird : IBird
{
    public void Move()
    {
        Fly();
    }

    public virtual void Fly()
    {
        Console.WriteLine("Flying");
    }
}
Enter fullscreen mode Exit fullscreen mode

The FlyingBird class implements the IBird interface and provides a Fly method. The Move method called the Fly method, represents the flying behavior.

Ostrich Class

public class Ostrich : IBird
{
    public void Move()
    {
        Run();
    }

    public void Run()
    {
        Console.WriteLine("Running");
    }
}
Enter fullscreen mode Exit fullscreen mode

The Ostrich class also implements the IBird interface but provides a Run method instead of Fly. The Move method is called the Run method, representing the running behavior.

Step 3. Use the Bird Classes.

Now we can use these bird classes in our program without violating the LSP.

public class Program
{
    public static void Main()
    {
        IBird bird1 = new FlyingBird();
        bird1.Move(); // Outputs: Flying

        IBird bird2 = new Ostrich();
        bird2.Move(); // Outputs: Running
    }
}
Enter fullscreen mode Exit fullscreen mode

In this refactored version.

  • FlyingBird and Ostrich both implement the IBird interface.
  • The Move method in each class encapsulates the specific movement behavior of the bird.
  • Calling Move on an instance of FlyingBird results in "Flying", and calling Move on an instance of Ostrich results in "Running".

Conclusion

By refactoring the original code to adhere to the Liskov Substitution Principle, we ensure that substituting subclasses for the base class does not break the program. This makes our code more robust, maintainable, and scalable.

Summary

  • The original code violated the LSP because substituting an Ostrich for a Bird caused an exception.
  • We introduced an interface IBird to represent common bird behavior.
  • We created specific bird classes (FlyingBird and Ostrich) that implemented the IBird interface and encapsulated their respective behaviors.
  • This approach ensures that our program behaves correctly regardless of the specific bird type used.

By following the Liskov Substitution Principle, we improve the design and reliability of our software, making it easier to understand, extend, and maintain.

Top comments (4)

Collapse
 
divyansh_chauhan_440e0ee5 profile image
Divyansh Chauhan

🔥🔥

Collapse
 
dotnetfullstackdev profile image
DotNet Full Stack Dev

Thank you @divyansh_chauhan_440e0ee5 , Its an honour to me! you can check more on dotnet-fullstack-dev

Collapse
 
andr_barros_d07f9108f56f profile image
André Barros

Simple and direct👏

Collapse
 
dotnetfullstackdev profile image
DotNet Full Stack Dev

Thank you @andr_barros_d07f9108f56f . Its pleasure to hear! you can get more bytes here dotnet-fullstack-dev