Liskov Substitution Principle (LSP)

LSP is one of the five SOLID principles of object-oriented programming, introduced by Barbara Liskov in a 1987 paper. The principle emphasizes the importance of maintaining the expected behavior of a program when using inheritance and polymorphism.

In simpler terms, the LSP states that if you have a base class (let's call it Class A) and a derived class (Class B) that inherits from it, you should be able to substitute an object of Class B wherever an object of Class A is expected, without causing any issues or breaking the program's behavior.

The property inherited from the parent class should hold real-world significance for the child class.

Mathematical Definition of LSP:

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

Explanation:

Here x is an instance of class T and y is an instance of class S. T is the parent class of S. In the definition, ϕ represents the property of object x. If all objects of class T have ϕ as the property then all the objects of type S should also have this property. Since it is the child class of T. In other words, all the instances of class S should satisfy ϕ

The LSP Analogy: If Animal is the parent class and possesses the talking property, all subclasses should also share the same property, which holds true. However, if we assume a subclass such as Snail, it inherits the talking property. Yet, in reality, Snails cannot communicate. Consequently, substituting an Animal object with a Snail object would fail the talking property. Thus, an LSP failure occurs when the inherited property lacks genuine significance.

Example:

In this example, we have a base class Animal with a talk() method, and three subclasses: Dog, Cat, and Snail. Each subclass overrides the talk() method to provide its specific behavior.

The main method demonstrates the LSP by creating instances of the derived classes (Dog, Cat, and Snail) and treating them as instances of the base class (Animal), while also calculating the length of the string returned by the talk() function.

class Animal {
    public String talk() {
        return "Animal is talking";
    }
}

class Dog extends Animal {
    @Override
    public String talk() {
        return "Dog is barking";
    }
}

class Cat extends Animal {
    @Override
    public String talk() {
        return "Cat is meowing";
    }
}

class Snail extends Animal {

    // Snails can't talk, so return null or return some Exception
    @Override
    public String talk() {
        return null;
    }
}

public class LSPExample {
    public static void main(String[] args) {
        Animal animal1 = new Dog();
        Animal animal2 = new Cat();
        Animal animal3 = new Snail();

        String talk1 = animal1.talk();
        String talk2 = animal2.talk();
        String talk3 = animal3.talk();

        System.out.println(talk1 + " (Length: " + talk1.length() + ")");
        System.out.println(talk2 + " (Length: " + talk2.length() + ")");
        System.out.println(talk3 + " (Length: " + talk3.length() + ")");
    }
}

Note: The Snail class must compulsorily override the talk() method since snails cannot talk. Therefore, it should either return null or throw an exception.

According to the LSP, if we replace an object of the base class (Animal) with an object of the derived class (Snail), we should still expect the same behavior and behavior-related properties. However, the Snail class's talk() method returns null, which is a departure from the behavior of its parent class (Animal).

In the LSPExample class, when instances of Animal, Dog, Cat, and Snail are created and their talk() methods are called, it is expected that each instance would return a string indicating the sound it makes. However, when talk() is called on a Snail instance, it returns null, which is unexpected behavior and can potentially lead to errors or unexpected program behavior.

This violation becomes more apparent when we attempt to retrieve the length of the talk strings for each instance. Since the talk() method of the Snail class returns null, attempting to retrieve the length of null using the .length() method results in a NullPointerException. This further demonstrates that the Snail class's behavior does not conform to the behavior expected from its base class and other derived classes, thereby violating the Liskov Substitution Principle.

Conclusion:

The LSP primarily focuses on the inheritance properties of the parent class. If a particular feature lacks real-world relevance for the child class, then the need arises to restructure the class inheritance.