In this article, we will learn the C# Liskov Substitution Principle and understand implementation to build scalable and easier to maintain software applications.
Table of Contents
- 1 Understanding the Liskov Substitution Principle
- 2 Applying the Liskov Substitution Principle
- 3 Key points of the Liskov Substitution Principle (LSP)
- 4 FAQs: Liskov Substitution Principle
- 4.1 Q1: What is the Liskov Substitution Principle?
- 4.2 Q2: Why is the Liskov Substitution Principle important?
- 4.3 Q3: How does the Liskov Substitution Principle prevent unexpected behavior?
- 4.4 Q4: What is an example of the Liskov substitution Principle?
- 4.5 Q5: What are the benefits of the Liskov Substitution Principle?
- 4.6 Related
Understanding the Liskov Substitution Principle
The Liskov Substitution Principle is one of the five SOLID principles of OOP, each of which addresses a specific characteristic of software design.
Remember that the Liskov Substitution Principle derives its name from Barbara Liskov, who initially conceptualized it in 1988.
- Single Responsibility Principle (SRP)
- Open-closed Principle (OCP)
- Liskov substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
What is the Liskov Substitution Principle?
The Liskov Substitution Principle states that the object of a derived class should be substitutable in place of the base class object without causing bugs or unexpected behaviour in the existing program.
In simple terms, LSP says that if a child class “B” is derived from another base class “A”, then the object of class “B” should be able to replace the object of class “A” without any issue.
Example: Without Liskov Substitution Principle
Let’s consider an example involving a class hierarchy for various bird species. We’ll create a base class Bird
and two derived classes Penguin
and Eagle
. The Bird
class has a method Fly()
that represents the ability to fly.
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("This bird can fly.");
}
}
public class Penguin : Bird
{
public override void Fly()
{
Console.WriteLine("Penguins cannot fly.");
}
}
public class Eagle : Bird
{
public override void Fly()
{
Console.WriteLine("Eagles can fly at great heights.");
}
}
In this scenario, we have two types of birds: Penguins and Eagles. Both of them are subclasses of the Bird class.
However, the Penguin subclass modifies the Fly() method to reflect that penguins are unable to fly, whereas the Eagle subclass keeps the default behavior of flying.
Problem Without Liskov Substitution Principle
The issue arises when we try to use the Fly()
method without considering the Liskov Substitution Principle. Let’s see what happens:
static void Main(string[] args)
{
Bird[] birds = { new Bird(), new Penguin(), new Eagle() };
foreach (var bird in birds)
{
bird.Fly();
}
}
Output:
While the above output seems fine, a problem arises when we attempt to treat all birds uniformly without accounting for their specific abilities.
In this case, we expect all birds to have the ability to fly due to their common base class, but the Penguin class undermines this expectation. The code violates the Liskov Substitution Principle because the Penguin class doesn’t honour the behavior of the base class, leading to confusion and potential errors in the code.
Applying the Liskov Substitution Principle
To address this issue, we can restructure our design by either allowing the Penguin class to fly (even if it’s a pretend of swimming instead of flying) or by revising our class hierarchy.
Let’s opt for the second approach and introduce an intermediate classes FlyingBird
and SwimmingBird
:
using System;
public class Bird
{
public virtual void PerformAction()
{
Console.WriteLine("This bird can perform an action.");
}
}
public class FlyingBird : Bird
{
public override void PerformAction()
{
Console.WriteLine("This bird can fly.");
}
}
public class SwimmingBird : Bird
{
public override void PerformAction()
{
Console.WriteLine("This bird can swim.");
}
}
public class Penguin : SwimmingBird
{
public override void PerformAction()
{
Console.WriteLine("Penguins are great swimmers.");
}
}
public class Eagle : FlyingBird
{
public override void PerformAction()
{
Console.WriteLine("Eagles can fly at great heights.");
}
}
class Program
{
static void Main(string[] args)
{
Bird[] birds = { new Bird(), new Penguin(), new Eagle() };
foreach (var bird in birds)
{
bird.PerformAction();
}
Console.ReadKey();
}
}
Output:
This bird can perform an action.
Penguins are great swimmers.
Eagles can fly at great heights.
Explanation of the Improved Code:
- We have a base class
Bird
that defines a virtual methodPerformAction()
. This method is designed to showcase a general action that birds can perform. - We also created two specialized bird categories:
FlyingBird
andSwimmingBird
. These classes inherit from theBird
class and override thePerformAction()
method to specify their respective capabilities. - The
Penguin
class is derived fromSwimmingBird
and further refines the behavior by specifying that penguins are great swimmers. - The
Eagle
class is derived fromFlyingBird
and specifies that eagles can fly at great heights. - In the
Main
method, we create an array of different bird instances, including instances of the base class and its derived classes. - We iterate through the array and call the
PerformAction()
method for each bird. This demonstrates that each bird class exhibits its unique behavior as intended.
Our code maintains consistent behavior across different bird categories by applying the Liskov Substitution Principle. This approach ensures that substituting derived classes for their base class doesn’t lead to unexpected outcomes, and the code remains more understandable and maintainable.
Key points of the Liskov Substitution Principle (LSP)
Here are the key points of the Liskov Substitution Principle (LSP) explained:
- Behavior Preservation: When you create a new type (class) derived from an existing type (base class), the new type should behave in a way consistent with the base type. In other words, you should keep how the base type behaves when you create a new version of it.
- Method Signatures: If you’re creating a new type from a base type and you override a method from the base type, the new method in the derived class should have the same name, take the same kind of input (parameters) and provide the same kind of output (return type). This way, when you replace the base type with the derived type, the code that uses these methods doesn’t break.
- Exception Handling: If the base type’s methods can throw certain types of errors, the derived type’s methods should also throw the same or closely related errors. This ensures that the code using the base type isn’t surprised by new errors when using the derived type.
- Invariants: When dealing with classes and objects, certain conditions or rules are always supposed to be true. These are called invariants. If the base type establishes certain invariants, the derived type should uphold those invariants too. This maintains the expected behavior and properties of the classes even after inheritance.
FAQs: Liskov Substitution Principle
Q1: What is the Liskov Substitution Principle?
The Liskov Substitution Principle
, also known as LSP, is a core principle in object-oriented programming. It highlights the importance of ensuring that derived classes can be used as replacements for their base classes without causing any issues with the correctness of the program.
It means that the behavior of the derived class should be the same as the base class rather than contradict it.
Q2: Why is the Liskov Substitution Principle important?
The Liskov Substitution Principle ensures that new classes derived from existing ones can be used seamlessly in place of their base classes. This promotes code reliability, reusability, and maintainability and allows us to extend and modify the software more efficiently.
Q3: How does the Liskov Substitution Principle prevent unexpected behavior?
By following the Liskov Substitution Principle (LSP), derived classes maintain the same behavior and method signatures as their base classes. This prevents surprises when replacing instances of the base class with instances of derived classes, as the expected behavior is maintained.
Q4: What is an example of the Liskov substitution Principle?
Certainly! Consider a scenario with a base class Bird
and derived classes Eagle
and Penguin
. The Liskov Substitution Principle ensures that even though eagles and penguins have different abilities (flying and swimming, respectively), they can be used interchangeably as instances of the base class Bird without causing unexpected issues. This principle promotes the smooth integration of derived classes into existing code.
Q5: What are the benefits of the Liskov Substitution Principle?
By following the Liskov Substitution Principle, you ensure that when you introduce new classes derived from existing ones, they seamlessly fit into your program’s structure without causing surprises or breaking existing functionality.
Articles you might also like:
- C# Tutorial
- SOLID Design Principles in C#
- Design Patterns
- Singleton Design Pattern in C#: A Beginner’s Guide with Examples
- Abstract Factory Design Pattern in C#: Real-World Example and Code Explanations
- Prototype Design Pattern: Everything You Need to Know
- Difference Between Array And ArrayList In C#: Choosing the Right Collection - May 28, 2024
- C# Program to Capitalize the First Character of Each Word in a String - February 21, 2024
- C# Program to Find the Longest Word in a String - February 19, 2024