Back to: SOLID Design Principles in C#
Liskov Substitution Principle in C# with Examples
In this article, I am going to discuss the Liskov Substitution Principle in C# with Examples. Please read our previous article before proceeding to this article, where we discussed the Open-Closed Principle in C# with Examples. The Letter L in SOLID stands for Liskov Substitution Principle, also known as LSP. As part of this article, we are going to discuss the following pointers in detail.
- What is Liskov Substitution Principle in C#?
- Example Without using the Liskov Substitution Principle in C#
- Example Using the Liskov Substitution Principle in C#
- Advantages and Disadvantages of the Liskov Substitution Principle in C#.
- How to Use Liskov Substitution Principle in C#?
What is the Liskov Substitution Principle in C#?
The Liskov Substitution Principle is a Substitutability principle in object-oriented programming Language. This principle states that if S is a subtype of T, then objects of type T should be replaced with objects of type S.
So, the Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class. That means child class objects should be able to replace parent class objects without compromising application integrity.
In simple words, we can say that when we have Parent-Child relationships, i.e., Inheritance Relationships between two classes, then if we successfully replace the object/instance of a parent class with an object/instance of the child class without affecting the behavior of the base class instance, then it is said to be in Liskov Substitution Principle. If you are not getting this point properly, don’t worry, we will see some real-time examples to understand this concept.
For example, a father is a teacher, whereas his son is a doctor. So here, in this case, the son can’t simply replace his father even though both belong to the same family.
Example Without using the Liskov Substitution Principle in C#:
Let us first understand one example without using the Liskov Substitution Principle in C#. We will see the problem if we are not following the Liskov Substitution Principle, and then we will see how we can overcome such problems using Liskov Substitution Principle. In the following example, first, we create the Apple class with the method GetColor. Then we create the Orange class, which inherits the Apple class as well as overrides the GetColor method of the Apple class. The point is that an Orange cannot be replaced by an Apple, which results in printing the color of the apple as Orange, as shown in the below example.
using System; namespace SOLID_PRINCIPLES.LSP { class Program { static void Main(string[] args) { Apple apple = new Orange(); Console.WriteLine(apple.GetColor()); } } public class Apple { public virtual string GetColor() { return "Red"; } } public class Orange : Apple { public override string GetColor() { return "Orange"; } } }
As you can see in the above example, Apple is the base class, and Orange is the child class, i.e., there is a Parent-Child relationship. So, we can store the child class object in the Parent class Reference variable, i.e., Apple apple = new Orange(); and when we call the GetColor, i.e., apple.GetColor(), then we are getting the color Orange, not the color of an Apple. That means the behavior changes once the child object is replaced, i.e., Apple stores the Orange object. This is against the LSP Principle. The Liskov Substitution Principle states that even if the child object is replaced with the parent, the behavior should not be changed. So, in this case, if we are getting the color of Apple instead of Orange, then it follows the Liskov Substitution Principle. That means there is some issue with our software design. Let us see how to overcome the design issue and makes the application follow Liskov Substitution Principle using C# Langauge.
Example Using the Liskov Substitution Principle in C#
Let’s modify the previous example to follow the Liskov Substitution Principle using C# Language. Here, first, we need a generic base Interface, i.e., IFruit, which is going to be the base class for both Apple and Orange classes. Now, you can replace the IFruit variable can be replaced with its subtypes, either Apple or Orage, and it will behave correctly. In the code below, we created the super IFruit as an interface with the GetColor method. Then the Apple and Orange classes were inherited from the Fruit class and implemented the GetColor method.
using System; namespace SOLID_PRINCIPLES.LSP { class Program { static void Main(string[] args) { IFruit fruit = new Orange(); Console.WriteLine($"Color of Orange: {fruit.GetColor()}"); fruit = new Apple(); Console.WriteLine($"Color of Apple: {fruit.GetColor()}"); Console.ReadKey(); } } public interface IFruit { string GetColor(); } public class Apple : IFruit { public string GetColor() { return "Red"; } } public class Orange : IFruit { public string GetColor() { return "Orange"; } } }
Now, run the application, and it should give the output as expected, as shown in the below image. Here we are following the LSP as we are now able to change the object with its subtype without affecting the behavior.
So, now Fruit can be any type and any color, but orange cannot be the color red. An apple cannot be of the color orange, meaning we cannot replace an orange with an apple, but fruit can be replaced with an orange or an apple because they are both Fruits; an apple is not an orange, and an orange is not an apple.
Note: You need to remember that, as we have the Inheritance concept, that does not mean we can randomly create the relationship between classes. We will not get any error or exception, but the behavior that we expect might not get. So, always make sure that the relationship and functionality that we are providing make sense. If it makes sense, then go for the inheritance relationship; if not, then don’t go for the inheritance relationship.
Advantages and Disadvantages of the Liskov Substitution Principle in C#:
The Liskov Substitution Principle (LSP) is one of the SOLID principles of object-oriented design. It emphasizes that objects of a derived class must be able to replace objects of the base class without affecting the correctness of the program. In simpler terms, derived classes should be substitutable for their base classes without causing unexpected behavior. Let’s explore the advantages and disadvantages of following the Liskov Substitution Principle in C#:
Advantages:
- Behavioral Consistency: Adhering to the LSP ensures that derived classes behave consistently with their base classes. This allows developers to reason about the behavior of objects more reliably.
- Code Reusability: Well-designed subclasses can reuse the functionality of their base classes, leading to more efficient and less duplicated code.
- Flexibility and Extensibility: New subclasses can be introduced without affecting the existing base class code. This makes the system more adaptable to changes.
- Interchangeability: LSP-compliant objects can be interchanged in a program without altering its correctness. This allows for easy substitution and testing.
- Design by Contract: LSP encourages defining clear contracts (interfaces or base classes) that specify the expected behavior of subclasses. This promotes better communication between developers.
- Support for Polymorphism: LSP enables the effective use of polymorphism, allowing a single interface to be implemented by multiple classes with different behavior.
Disadvantages:
- Violations Can Be Subtle: Violations of the Liskov Substitution Principle can sometimes be subtle and difficult to detect. Unexpected behavior might occur at runtime when replacing base class objects with derived class objects.
- Complexity in Inheritance Hierarchies: Inheritance hierarchies that become too deep or complex can make it challenging to maintain and ensure compliance with LSP.
- Potential Overhead: The need to adhere to the contract specified by the base class can sometimes lead to additional overhead when implementing derived classes.
- Design Challenges: Designing base classes and interfaces that allow for proper substitution can sometimes be challenging and require careful planning.
- Semantic Issues: If base classes do not have well-defined contracts or behaviors, achieving proper LSP compliance cannot be easy.
- Trade-offs with Performance: In some cases, adhering strictly to LSP might require additional runtime checks, which can impact performance.
The Liskov Substitution Principle provides several advantages by ensuring behavioral consistency, code reusability, and flexibility in object-oriented design. However, it requires careful consideration of the relationships between the base and derived classes to avoid subtle runtime issues. Adhering to the LSP leads to more robust and maintainable code, but it might involve some trade-offs and design challenges along the way.
How to Use Liskov Substitution Principle in C#?
Using the Liskov Substitution Principle (LSP) in C# involves designing your classes and inheritance hierarchies to ensure derived classes can be substituted for their base classes without causing unexpected behavior. Here’s a step-by-step guide on how to use the Liskov Substitution Principle effectively in C#:
- Identify the Base Class or Interface: Identify the base class or interface that defines the common contract or behavior that derived classes should adhere to.
- Follow the Contract: Ensure that derived classes implement the same contract or behavior defined by the base class or interface. This includes implementing methods, properties, and behavior as specified.
- Override Methods Carefully: When overriding methods in derived classes, make sure that the overridden methods adhere to the expected behavior defined by the base class. Avoid changing the semantics of the method.
- No Weakening Preconditions: Ensure that preconditions (parameters, constraints, etc.) required by the base class methods are not weakened in derived class methods. Derived class methods should meet or strengthen the preconditions.
- No Strengthening Postconditions: Derived class methods should not strengthen the base class methods’ postconditions (return values, effects, etc.). The behavior should be consistent or more relaxed, but not more strict.
- Avoid Empty Overrides: Avoid overriding methods in derived classes with empty or no-op implementations. This violates the LSP because the derived class is not substitutable for the base class.
- Use Polymorphism: Utilize polymorphism to treat objects of derived classes as objects of the base class. This allows you to interchangeably use different implementations without affecting correctness.
- Test Substitutability: Test the substitutability of derived classes by using them in contexts where base class instances are expected. Ensure that the behavior remains consistent and expected.
- Document Contracts: Clearly document base classes and interfaces’ contracts (interfaces, abstract methods, behavior). This helps developers understand the expected behavior when implementing derived classes.
- Refactor for Consistency: If you encounter violations of the LSP, refactor your code to ensure that the derived classes adhere to the contract and behavior defined by the base class.
Here’s a simple example demonstrating the use of the Liskov Substitution Principle in C#. In the below example, both Circle and Square classes adhere to the contract defined by the Shape base class. They can be treated interchangeably in the shapes collection, demonstrating the Liskov Substitution Principle.
using System.Collections.Generic; using System; namespace SOLID_PRINCIPLES.LSP { public class Program { static void Main(string[] args) { // Usage List<Shape> shapes = new List<Shape> { new Circle { Radius = 5 }, new Square { SideLength = 4 } }; foreach (Shape shape in shapes) { Console.WriteLine($"Area of : {shape.Area()}"); } Console.ReadKey(); } } public abstract class Shape { public abstract double Area(); } public class Circle : Shape { public double Radius { get; set; } public override double Area() { return Math.PI * Radius * Radius; } } public class Square : Shape { public double SideLength { get; set; } public override double Area() { return SideLength * SideLength; } } }
By following these steps and designing your classes with a focus on adhering to contracts and maintaining expected behaviors, you can effectively use the Liskov Substitution Principle to create maintainable, extensible, and robust object-oriented systems in C#.
In the next article, I am going to discuss the Interface Segregation Principle in C# with Examples. In this article, I try to explain the Liskov Substitution Principle in C# with Examples. I hope you enjoy this Liskov Substitution Principle article.
Thank you. Very good example and well explained.
good explanation with real time example.
I can do the same implementation without creating an interface.
Apple apple = new Orange();
Console.WriteLine(apple.GetColor());
apple = new Apple();
Console.WriteLine(apple.GetColor());
It will gives the output as expected.
Could please explain with any other example.
see it is mentioned LSP states begaviour should not be changed
your code is changing behaviour
override is used to change the behaviour
Apple apple = new Orange();
this will give output as orange. As per principle it should not print “The point is that an Orange cannot be replaced by an Apple, which results in printing the color of apple as Orange”.
So if you call by Abstract object it will not allow you to create object like this
Apple apple = new Orange();
for the example without using the Liskov Substitution Principle, you should remove the “virtual” keyword and replace the “override” by “new” instead. So, it will more understandable.
I think your example that without using the Liskov Substitution Principle is wrong, it follows the principal.
Apple apple = new Orange();
Console.WriteLine(apple.GetColor());
Orange apple1 = new Orange();
Console.WriteLine(apple1.GetColor());
I agree with PHINEAS VUONG, with new key word will not follow the principal
i think a way it might be more clear is to say Fruit can be any type and any color, but a orange cannot be the color red and an apple cannot be of color orange , meaning we cannot replace a orange with an apple but fruit can be replaced with an orange or an apple because they are both Fruits, a apple is not an orange and a orange is not a apple.
Lets say the base class was Cooldrinks, and a sub class was Coke and the other Fanta Orange, then we would be able to replace the Base Type (cooldrinks) with either Coke or Fanta Orange because they are both Cooldrinks but if we ask for a Coke we do not expect or want a Fanta we want a Coke , if we ask for cooldrink we do not know what we will receive and any of the 2 would be sufficient. So if the base type is the same any of the 2 would work but we specifically want one sub type another would not be sufficient.
Hope this helps out.
Great explanation. The comment added by DIEDERIK clearing the all the confusion.
Very useful and understandable content. Thanks for your time.
Very Confusing! I fully agree with DIEDERIK.
We can also achieve using the interface for Liskov Substitution Principle in C#
using System;
namespace With_Liskov_Substitution_Principle
{
interface Fruit
{
string GetColor();
}
public class Apple : Fruit
{
public string GetColor()
{
return “Red”;
}
}
public class Orange : Fruit
{
public string GetColor()
{
return “Orange”;
}
}
class Program
{
static void Main(string[] args)
{
Orange a = new Orange();
Console.WriteLine(a.GetColor());
Console.ReadKey();
}
}
}
I am immediately impressed by the author’s expertise and by his lucid explanation of the work.
I think this example is not correct , because when I substitute base class instance with child class instance I get the correct response
Apple fruit1 = new Orange();
Console.WriteLine(fruit1.GetColor()); //orange
Apple fruit2 = new Apple();
Console.WriteLine(fruit2.GetColor()); // red
Thanks