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;
    }
}

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

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;
    }
}

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

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.

Did you find this article valuable?

Support Theodoros Karropoulos by becoming a sponsor. Any amount is appreciated!