Blog for Junior Developers C#/.NET

Monday, July 01, 2024

Another SOLID principle that will make our code of good quality is the Liskov Substitution Principle (LSP) was developed in 1988 by American programmer Barbara Liskov. For the first time the rule was:

"We are looking for the following substitution property: If for every object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T the behavior of P remains unchanged when o1 is substituted for o2, then S is a subtype of T."

Actually, I could end this article here, because I think everything is clear now. True? Well, probably not entirely :)

In the final version, the content of the Liskov substitution rule reads as follows:

"Functions that use pointers or references to base classes must also be able to use objects of classes that inherit from base classes, without knowing those objects precisely."

In short, you can substitute any type of a derived class in place of the base type and it should not lose proper operation.


solid-lsp-everything-you-should-know-about-the-liskov-substitution-principle.jpg

SOLID - LSP - Everything You Should Know About Liskov's Substitution Principle

The Liskov substitution principle is related to the principle discussed in the previous article, the open-closed principle, because thanks to the ability to replace subtypes, we can expand classes without having to modify them. For inheritance to be good, derived classes should not overwrite the methods of base classes. However, they can be extended by calling a method from the base class, i.e. the derived class should extend the base class without affecting its operation.

The LSP principle applies to properly designed inheritance. If we create a derived class, we must also be able to use it instead of the base class. Otherwise, inheritance has been implemented incorrectly.

Let's start with a popular example that very well shows the violation of the LSP principle, there is even a famous meme on this topic. I think this example will best illustrate what the LSP principle is.


liskov-substitution-principle-ducks.jpg

So if something looks like a duck, quacks like a duck, but needs batteries, you probably have a bad abstraction :)


public interface IDuck
{
    void Swim();
    bool IsSwimming { get; }
}

public class OrganicDuck : IDuck
{
    private bool _isSwimming = false;
    public void Swim()
    {
        Console.WriteLine("OrganicDuck swims");
        _isSwimming = true;
    }

    public bool IsSwimming { get { return _isSwimming; } }
}

public class ElectricDuck : IDuck
{
    private bool _isSwimming;

    public void Swim()
    {
        if (!IsTurnedOn)
            return;

        Console.WriteLine("ElectricDuck swims");
        _isSwimming = true;
    }

    public bool IsTurnedOn { get; set; }
    public bool IsSwimming { get { return _isSwimming; } }
}	

public class Program
{
    static void Main()
    {
        var ducks = new List<IDuck>();
        IDuck organicDuck = new OrganicDuck();
        IDuck electricDuck = new ElectricDuck();
        ducks.Add(organicDuck);
        ducks.Add(electricDuck);

        MakeDuckSwim(ducks); //OrganicDuck swims
    }

    private static void MakeDuckSwim(IEnumerable<IDuck> ducks)
    {
        foreach (var duck in ducks)
            duck.Swim();
    }
} 

As you can see, both ducks implement the IDuck interface. They contain Swim methods, but when this method is called, the electric duck does not swim because it has not been enabled before. If you wanted to "fix" this code so that both ducks swim, you would need to modify the MakeDuckSwim method and make separate logic in this method when the duck is of type ElectricDuck.

private static void MakeDuckSwim(IEnumerable<IDuck> ducks)
{
    foreach (var duck in ducks)
    {
        if (duck is ElectricDuck)        
            ((ElectricDuck)duck).TurnOn();
        
        duck.Swim(); 
    }
} 

Such a change is, of course, also bad and violates the LSP and OCP principles (a class is not closed to modifications). To sum up, we definitely have the wrong abstraction designed in this case.

As the definition says, functions that use references to base class pointers should also be able to use derived class functions without knowing the object. This means that we can say that the Liskov substitution principle, among other things, comes down to the prohibition of asking about the type of an object.


Additionally, for the LSP principle to be observed, the following conditions must also be met:


#1 Covariance of return types in a subtype.
#2 Contravariance of method arguments in a subtype.
#3 Subtype methods should not throw any new exceptions, except when the new exceptions are subtypes of the supertype's reported methods.
#4 Preconditions cannot be strengthened in a subtype.
#5 Subconditions cannot be less restrictive in a subtype.

Let's discuss each of these conditions with an example.


#1 Covariance of return types in a subtype.


Covariance describes the relationship between classes. Take a look. please the following example:

public class Vehicle 
{
}

public class Car : Vehicle
{
}

public class Audi : Car
{
}

public class Program
{
    private static Car GetCar()
    {
        return new Car();
    }

    static void Main()
    {
        Vehicle vehicle = GetCar();
        Car car = GetCar();
        Audi audi = GetCar(); //Compilation error
    }
} 

I presented a simple inheritance relationship. The Vehicle class is the base class and is inherited by the Car class and the Audi class. Then, in the Main method, it tries to assign an object of the Car type to each type. Fortunately, the compiler does not allow this, because we are trying to assign a more specific type to the Audi type. Covariance is a conversion from a more specific type to a more general type. This means that for a derived type we can always assign an object type or a derived type, but not a base type.


#2 Contravariance of method arguments in a subtype.

Contravariance is the inverse relationship to covariance and therefore allows conversion from a more general type to a more specific type.

public class Program
{
    private static void TurnOn(Car car)
    {
    }

    static void Main()
    {
        TurnOn(new Vehicle()); //Compilation error
        TurnOn(new Car());
        TurnOn(new Audi());
    }
} 

As you can see, the TurnOn method expects a parameter of the Car type, and when we try to pass the Vehicle type as an argument, we get a compilation error. Therefore, you cannot pass a type more general than Car. So, similarly to covariance, the compiler warns C# programmers against violating this rule.

#3 Subtype methods should not throw any new exceptions, except when the new exceptions are subtypes of the supertype's reported methods.


public class Vehicle
{
    public virtual void TurnOn()
    {
        throw new IndexOutOfRangeException();
    }
}

public class Car : Vehicle
{
    public override void TurnOn()
    {
        throw new DivideByZeroException();
    }
}

public class Program
{
    public static void TurnOnVehicle(Vehicle vehicle)
    {
        try
        {
            vehicle.TurnOn();
        }
        catch (IndexOutOfRangeException)
        {
        }
    }

    static void Main()
    {
        var vehicles = new List<Vehicle>
        {
            new Vehicle(),
            new Car()
        };

        foreach (var vehicle in vehicles)
        {
            TurnOnVehicle(vehicle); //Unhandled exception
        }
    }
} 

The above code violates this rule because the Car type will throw an exception in the Main method that the base type does not expect. In this situation, it would be possible to throw an exception in the Car class, which in this case would inherit from IndexOutOfRangeException.


#4 Preconditions cannot be strengthened in a subtype.


public class Vehicle
{
    public virtual void TurnOn(int temp)
    {
        if (temp < -20)
            return;

        //logic
    }
}

public class Car : Vehicle
{
    public override void TurnOn(int temp)
    {
        if (temp < -5)
            return;

        //logic
    }
} 

A derived class is more restrictive than its base type. Requires the temp argument to be greater than -5, where the base class only requires the argument to be greater than -20. The derived type from Car in this case must support the same range of data or a wider range, certainly not smaller. This is another violation of the LSP rule.


#5 Postconditions cannot be less restrictive in a subtype.


public class Vehicle
{
    public int Temp { get; set; }
    public virtual int GetTemp()
    {
        //logic
        Temp = -1;

        if (Temp < -100)
            throw new Exception("Sensor damaged.");

        return Temp;
    }
}

public class Car : Vehicle
{
    public override int GetTemp()
    {
        //logic
        Temp = -200;

        return Temp;
    }
} 

The base class will throw an exception when Temp < -100 and at least such a condition should be present in the derived class. Currently, the Car class does not check the Temp properties at all, which makes it less restrictive than the base Vehicle class. This is a violation of the LSP rule.


SUMMARY


The LSP principle is initially quite difficult to understand and programmers often confuse it with other SOLID principles: OCP (open-closed principle - discussed in the previous article) and ISP (interface segregation principle - I will write about it in the next article). When implementing Liskov's substitution principle correctly, we should not use any conditional constructs to force correct operation. A derived object must, from a logical point of view, be a special case of the base object. You must always remember that you can substitute any derived object in place of the base object and you cannot ask what class the object is.

Author of the article:
Kazimierz Szpin

KAZIMIERZ SZPIN
Software Developer C#/.NET, Freelancer. Specializes in ASP.NET Core, ASP.NET MVC, ASP.NET Web API, Blazor, WPF and Windows Forms.
Author of the blog CodeWithKazik.com

Previous article - SOLID - OCP - Everything You Should Know About the Open-Closed Principle
Next article - SOLID - ISP - Everything You Should Know About the Interface Segregation Principle
Comments (1)
Kazimierz Szpin
KAZIMIERZ SZPIN, czwartek, 11 lipca 2024 11:07
What do you think about this article? I'm curious about your opinion.
Dodaj komentarz
© Copyright 2024 CodeWithKazik.com. All rights reserved. Privacy policy.
Design by Code With Kazik and Modest Programmer.