DEV Community

Theodore Karropoulos
Theodore Karropoulos

Posted on • Edited on

Demystifying the Liskov Substitution Principle: A Guide for Developers

What Liskov Substitution Principle (LSP) is?

The Liskov Substitution Principle (LSP) is one of the fundamental principles in object-oriented programming (OOP) design. It was introduced by Barbara Liskov in 1987 and is part of the SOLID principles.

The Liskov Substitution Principle (LSP) states that any subclass of a superclass should be usable in place of its superclass without introducing errors or altering the expected behavior of the program. In simpler terms, if code is written to work with a specific type of object (the base class), it should also be able to work seamlessly with any of its subclasses (derived classes) without any issues or surprises.

I understand that the Liskov Substitution Principle (LSP) can be difficult to comprehend, and I struggled the most with this principle, both in theory and practical implementation. However, I ask for your patience as we navigate through it together.

What are the reasons for utilizing the Liskov Substitution Principle (LSP)?

There are several reasons why to use LSP, but I will narrow them down to the most important one.

Encourages Reusability and Modularity

By adhering to the Liskov Substitution Principle (LSP), we establish the ability to seamlessly substitute derived classes in place of their base classes. This adherence fosters code reusability, enabling us to apply the same code to different subclasses.

Enables Polymorphism

LSP empowers the utilization of polymorphism, a fundamental concept in object-oriented programming. Polymorphism enables the interchangeability of objects from various classes, based on their shared base class.

Supports Abstraction and Interface Contracts

LSP plays a crucial role in establishing and preserving abstraction by guaranteeing that derived classes uphold the contracts defined by their base classes.

Enhances Maintainability and Flexibility

Through adherence LSP, we enhance the maintainability and flexibility of our codebase. LSP reduces code duplication, fosters a clear and consistent structure, and enables modifications or additions to derived classes without impacting existing code, as long as they conform to the contracts of the base class.

Promotes Design by Contract
LSP highlights the significance of crafting classes with clearly defined contracts. These contracts outline the expected behavior and guarantee that a class offers to its clients. By faithfully adhering to these contracts, LSP ensures that derived classes uphold identical guarantees and behaviors. As a result, the code becomes more reliable and robust.

Code Example

Now, let's consider an example that helps us visualize and grasp the concept more effectively through code. As developers, code serves as our language, and it often proves to be the most straightforward approach for conveying ideas. Let's first examine a poorly designed code example that neglects the use of the Liskov Substitution Principle (LSP).

public class Rectangle
{
    public int Width { get; set; }
    public int Height { get; set; }

    public virtual int CalculateArea()
    {
        return Width * Height;
    }
}

public class Square : Rectangle
{
    public override int CalculateArea()
    {
        return Width * Width;
    }
}

Enter fullscreen mode Exit fullscreen mode

In this bad implementation, we have a Rectangle class and a Square class derived from it. The issue arises from the fact that Square is a special case of a Rectangle where all sides are equal. However, by inheriting from Rectangle, we violate the LSP.

The violation becomes apparent when we modify the dimensions of a Square:

Rectangle rectangle = new Square();
rectangle.Width = 5;
rectangle.Height = 3;

Console.WriteLine($"Area: {rectangle.CalculateArea()}"); // Incorrect result: 25 instead of 15
Enter fullscreen mode Exit fullscreen mode

Here, we expected the area to be 15 (5 * 3) since we set width to 5 and height to 3. However, due to the LSP violation, the overridden CalculateArea() method in Square calculates the area based only on the width, resulting in an incorrect area of 25 (5 * 5).

Now, let's proceed to redesign our implementation while applying the principles of the Liskov Substitution Principle (LSP).

public abstract class Shape
{
    public abstract int CalculateArea();
}

public class Rectangle : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }

    public override int CalculateArea()
    {
        return Width * Height;
    }
}

public class Square : Shape
{
    public int Side { get; set; }

    public override int CalculateArea()
    {
        return Side * Side;
    }
}

Enter fullscreen mode Exit fullscreen mode

In the refactored implementation, both Rectangle and Square inherits from the abstract base class Shape. Each class provides its implementation of the CalculateArea() method, respecting the specific behavior of their shape.

Now, we can use the classes as follows:

Shape rectangle = new Rectangle
{
    Height = 3,
    Width = 5
};

Shape square = new Square
{
    Side = 3
};

Console.WriteLine($"Rectangle area: {rectangle.CalculateArea()}"); // Output: 15
Console.WriteLine($"Square area: {square.CalculateArea()}"); // Output: 9
Enter fullscreen mode Exit fullscreen mode

Furthermore, our code facilitates effortless extension by accommodating the addition of new shapes and enabling the calculation of their respective areas.

By adhering to the Liskov Substitution Principle (LSP), we establish a flexible foundation that allows for the seamless integration of additional shapes into the existing codebase. This extensibility promotes maintainability and supports the calculation of areas for various shapes without significant modifications or complications.

In essence, the Liskov Substitution Principle (LSP) offers guidance for constructing class hierarchies that foster code reusability, maintainability, and extensibility. By upholding LSP, we establish a foundation where derived classes seamlessly substitute the base class, fostering polymorphic behavior and facilitating smoother code maintenance and future improvements. On the other hand, disregarding LSP can introduce fragility, impair code comprehensibility, and create challenges when extending the system. It is important to recognize that adhering to LSP empowers us to design flexible, robust systems while neglecting it can hinder system reliability and impede future system growth.

Top comments (5)

Collapse
 
arnoldatse profile image
Arnold Atsé

Thank you for your getting time to explain all of that however, I want to know.
The LSP can only be applyed with Abstract parent class or it's possible with concret class, if yes it's possible with concret class, can you give an exemple usecase?

Collapse
 
tkarropoulos profile image
Theodore Karropoulos

Hello @arnoldatse and thanks for getting the time to read my article! Yes, you can apply the Liskov Substitution Principle (LSP) using concrete classes along with interfaces. However, my preference for using abstractions, such as interfaces or abstract classes, is because they help achieve reduced coupling and increased flexibility. Additionally, abstractions provide clear expectations and improve testing. Below, I present an example of how to apply LSP with concrete classes.

public class Shape
{
    public virtual int CalculateArea()
    {
        return 0;
     }
}

public class Rectangle : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }

    public override int CalculateArea()
    {
        return Width * Height;
     }
}
Enter fullscreen mode Exit fullscreen mode

I hope my response was helpful. If you have any other questions, please don’t hesitate to reach out.

Collapse
 
arnoldatse profile image
Arnold Atsé • Edited

Haaa ok understood, so according LSP all child class has to update parent class behavior in the case of parent concret class so that parent and child class can be substitute without break down the app.

In this case so I have preference to abstraction like you.

Thank you to get me out of this torment of understanding how to well apply this principle. 😅

Collapse
 
gidebn profile image
Gideon

In which part do you substitute a subclass by the superclass or apply method overwriting in this example?

Collapse
 
tkarropoulos profile image
Theodore Karropoulos

Hello @gidebn and thanks for your comment and for getting the time to read my article!
The substitution of a child class with the parent class occurs during the object instantiation. Specifically when the creation of the child classes Rectangle and Square take place and assign them to the variables of the parent class Shape, the substitution happens implicitly.