We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Understanding Solid Principles - The Liskov Substitution Principle

SOLID Principles  • Posted 6 months ago

The third principle of the SOLID principles, which are five principles describing few fundamental rules for developing arguably ideal software components which are scalable, extensible, robust and flexible, is the Liskov Substitution Principle. This principle can be termed as one of the basic foundations of the Object Oriented Design languages which talks mainly about substitutions and inheritance.

The principle in its exact terms is more of a mathematical description which is as follows:

"Let f(x) be a property provable about objects x of type T. Then f(y) should be true for objects y of type S where S is a subtype of T."

Translating the above principle in terms of programming, we get the below statement:

"The descending classes should behave the same way as the base classes behave. Which means the type S can be used in any place where a type T is used, where S is a subtype of T."

The ambiguity of Substitution - The Square Rectangle Problem:

The principle talks about the usage of derived types in place of base types and the anomalies it can create if the types are not properly created. For illustrating this, we take the classical example of mathematics "The Square Rectangle Problem". In mathematics, every Square "is-a" Rectangle - basically square is considered to be a special type of Rectangle with a peculiar feature of having equal length and breadth. This means that for every place where a Rectangle is used, a square can be substituted. But when we translate this mathematical paradigm into a programming component, we get into a scenario of ambiguity: A square area cannot be the same as a Rectangular area. Also, the class used for a square cannot be exactly substituted into a Rectangle: for the composition of the type changes.


    public class Rectangle
    {
        int length;
        int breadth;

        public Rectangle(int length, int breadth)
        {
            this.length = length;
            this.breadth = breadth;
        }

        public int CalculateArea()
        {
            return length * breadth;
        }

        public int CalculatePerimeter()
        {
            return 2 * (length + breadth);
        }
    }


    public class Sqaure : Rectangle
    {
        int side;

        public Sqaure(int length, int breadth) : base(length, breadth)
        {
            side = length = breadth;
        }

        public new int CalculateArea()
        {
            return side * side;
        }

        public new int CalculatePerimeter()
        {
            return 4 * side;
        }
    }

Now in a client, if we are to substitute a Square object into a Rectangle type - we end up in a rather cumbersome scenario.


    public class Client
    {
        Rectangle rectangle;
        
        public void CalculateArea(int length, int breadth)
        {
            rectangle = new Sqaure(length, breadth);
            Console.WriteLine(rectangle.CalculateArea());
        }
    }

In such scenarios, we cannot simply decide basing on the "is-a" characteristic of types. We need to look for the characteristic "is-substitutable-for" among the types which in this case is violated. A Square type cannot be substituted for a Rectangle type; although a Square type "is-a" Rectangle type.

Composition over Inheritance:

The Liskov Substitution Principle, brings together the concept of building types based on Composition rather than depending on base and derived types. In other words, we shall have a behavior provided by a type be described by means of abstractions and let those abstractions be used where ever the type needs to be utilized. Then we can have the abstraction be substituted by a variety of implementations of the type basing on the requirement and let the decision be made at the run-time rather than at the compile time. In this way we don't rely on inheritance for substitution and rather the abstractions have multiple compositions made which are all substitutable for the abstraction.

In the above Square-Rectangle problem, we can deal it in way such that we should create an abstraction for the behaviors these types are expected to contain: Area and Perimeter. So we create an interface IQuadrilateral which describes these two behaviors for both the Rectangle and Square.


    public interface IQuadrilateral
    {
        int CalculateArea();
        int CalculatePerimeter();
    }

And let both Square and Rectangle, for that matter any variant of a Quadrilateral, implement the behaviors defined by the interface IQuadrilateral.


    public class Rectangle : IQuadrilateral
    {
        int length;
        int breadth;

        public Rectangle(int length, int breadth)
        {
            this.length = length;
            this.breadth = breadth;
        }

        public int CalculateArea()
        {
            return length * breadth;
        }

        public int CalculatePerimeter()
        {
            return 2 * (length + breadth);
        }
    }

    public class Sqaure : IQuadrilateral
    {
        int side;

        public Sqaure(int side)
        {
            this.side = side;
        }

        public int CalculateArea()
        {
            return side * side;
        }

        public int CalculatePerimeter()
        {
            return 4 * side;
        }
    }

And finally, the client (AreaCalculator) can never know what is the type that is being created for it knows only the type IQuadrilateral that is being passed onto the client.


    public class AreaCalculator
    {
        public void CalculateArea(IQuadrilateral shape)
        {
            Console.WriteLine(shape.CalculateArea());
        }
    }

    public class Client
    {
        static void Main(String[] args)
        {
            AreaCalculator calci = new AreaCalculator();
            calci.CalculateArea(new Rectangle(1, 2));
            calci.CalculateArea(new Sqaure(5));
        }
    }

NULLs break LSP:

One of the most important violations are explicit NULL checks, since any type can be substituted by a NULL. And when you have an explicit type check for NULL you can never know what can be passed on for a type and when a NULL is passed it eventually breaks the system. And hence explicit NULL checking must be avoided for types and instead let the types decide how a NULL needs to be handled. This is further detailed by the NULL object design pattern. Other alternative for handling NULLs is by using Nullable types which have implicit NULL handling.

Fully Implemented Interfaces:

Another scenario where the LSP is violated is when we try to substitute an interface type with a partial implementation rather than a fully implemented derived types. Since the target types can never know what is being substituted for the base types they expect, and may invoke any behavior of the base type, it is quite a severe threat for the whole system when we leave certain behaviors without implementing them. NotImplementedExceptions must be avoided at any cost, and they are clear violations of LSP.

These are some of the features and implementation aspects described by the Liskov Substitution Principle. Complying to this rule presents safe and robust components developed which are also flexible for any implementation change.

Also Read:

Implementing NULL Object Pattern in ASP.NET Core

Published 6 months ago

Sponsored Links