Showing posts with label Liskov Substitution Principle. Show all posts
Showing posts with label Liskov Substitution Principle. Show all posts

Friday, September 20, 2024

SOLID Principles: Liskov Substitution Principle



In this series, I will highlight some areas where you can improve the quality of your code during the development process. In the first article, I explained why code quality is paramount. Then I introduced briefly some ideas that would help you improve the quality of your code. I will provide more details on these points in future articles. Some concepts will be illustrated with examples in Java and Python. I will begin this journey with you by introducing the SOLID principles in this article. They are some of the concepts that will help you improve the quality of your code and boost your abstract thinking in the object-oriented paradigm. After introducing the Single Responsibility Principle and Open/Closed Principle in previous articles, I will elucidate the Liskov Substitution Principle in this article, with examples in Java and Python.


1. Liskov Substitution Principle

This principle states that objects in a program should be replaceable by instances of their sub-types without modifying the accuracy of this program. In other words, if S is a sub-type of T, then instances of type T may be replaced with objects of type S.


Consider an example of a program in which ‘Car’, ‘Truck’, and ‘Van’ are sub-classes of the ‘Vehicle’ class. This program adheres to LSP when each occurrence of an object of the ‘Vehicle’ class could be replaced by one of its three sub-classes without any impact on the correctness of the program.


This principle ensures that any one of the sub-classes extends its super-class without altering its behavior. That means you should only insert in the parent class properties shared by all sub-classes.


On the other hand, this principle helps you keep your software class hierarchies consistent with the Open/Closed principle. We will confirm this later with the help of an example.


We can summarize the benefits of adhering to the LSP as follows:

- It helps prevent unexpected behavior and avoid opening closed classes to make changes.

- It allows for more predictable behavior of the model hierarchy, which will be easier to extend.

- It leads to better maintenance and testability.

- It helps ensure better loose coupling between certain system components.


2. Examples of LSP in Java

To illustrate the LSP, here we take two different examples in Java language. One adheres to LSP and the second violates it.

2.1. Example 1

Let’s take a simple example of a class ‘Bird’ and suggest the following code for it:


public class Bird {


    Double weight;

    public Bird(Double weight) {

        this.weight = weight;

    }

    public void eat() {

        System.out.println("I can eat");

    }

    public void fly() {

        System.out.println("I can fly");

    }

}


An eagle is a bird and it can extend the ‘Bird’ class correctly as follows:

public class Eagle extends Bird {

    public Eagle(Double weight) {

        super(weight);

    }

    @Override

    public void eat() {

        super.eat();

        System.out.println("I am carnivore");

    }

}


But a chicken is also a bird. So, the ‘Chicken’ class can extend the ‘Bird’ class as follows:

public class Chicken extends Bird {

    public Chicken(Double weight) {

        super(weight);

    }

    @Override

    public void eat() {

        super.eat();

        System.out.println("I am omnivore");

    }

    @Override

    public void fly() {

        throw new UnsupportedOperationException("Unsupported Operation");

    }

}


But we have a problem here! The chicken cannot fly and therefore cannot support the ‘fly’ method. In this case, you need to change the behavior of the super-class, like throwing an exception, which violates the Liskov Substitution Principle.
The principal cause of this problem is that flying is not a shared property by all birds. In other words, objects of the ‘Chicken’ sub-class cannot substitute objects of the ‘Bird’ super-class. We illustrate this issue using an example. 
In the ‘BirdDemo’ class, we define ‘birdFly’ as a method that takes a list of instances of the ‘Bird’ class. It iterates over the list and calls each bird’s ‘fly’ method. In the main method of the ‘BirdDemo’ class, we create two birds as instances of the ‘Eagle’ and ‘Chicken’ classes, respectively. Then we add these two instances to a list and call the ‘birdFly’ method with this list as an argument.

import java.util.List;


public class BirdDemo {

    public static void main(String[] args) {

        Bird bird = new Eagle(10.0);

        Bird chicken = new Chicken(5.5);

        List<Bird> birdsList = List.of(bird, chicken);

        birdFly(birdsList);

    }

    public static void birdFly(List<Bird> birdsList) {

        for (Bird bird : birdsList){

            bird.fly();

        }

    }

}


Running this demonstration gives us the following result:

I can fly

Exception in thread "main" java.lang.UnsupportedOperationException: Unsupported Operation
at lsp.app.violate.Chicken.fly(Chicken.java:17)
at lsp.app.violate.BirdDemo.birdFly(BirdDemo.java:17)
at lsp.app.violate.BirdDemo.main(BirdDemo.java:12)


To fix such an issue, you need to reopen the class (violating the Open/Closed Principle) and make changes. You should account for this behavior early in the requirements phase. We can suggest a better implementation that adheres to the Liskov Substitution Principle for these classes as follows:

public class Bird {

    Double weight;

    public Bird(Double weight) {

        this.weight = weight;

    }

    public void eat() {

        System.out.println("I can eat");

    }

}


public class FlyingBird extends Bird {

    public FlyingBird(Double weight) {

        super(weight);

    }

    public void fly() {

        System.out.println("I can fly");

    }

}


public class Eagle extends FlyingBird {

    public Eagle(Double weight) {

        super(weight);

    }

    @Override

    public void eat() {

        super.eat();

        System.out.println("I am carnivore");

    }

}


public class Chicken extends Bird {

    public Chicken(Double weight) {

        super(weight);

    }

    @Override

    public void eat() {

        super.eat();

        System.out.println("I am omnivore");

    }

}


2.2. Example 2

Let’s take the famous example of ‘Shape’. Any object of the class ‘Shape’ has an area and a perimeter. For that, we suggest two methods ‘getArea()’ and ‘getPerimeter()’ to implement such behavior. However, we return something like ‘0’ in these methods because we don’t have any information that helps us perform such calculations. These details can only be available if we know what shape it is. We should create new sub-classes like ‘Circle’ and ‘Triangle’ to achieve such a goal.

public class Shape {

    protected String name;

    public Shape(String name) {

        this.name = name;

    }

    public double getArea() {

        return 0.0;

    }

    public double getPerimeter(){

        return 0.0;

    }

}


public class Shape {

    protected String name;

    public Shape(String name) {

        this.name = name;

    }

    public double getArea() {

        return 0.0;

    }

    public double getPerimeter(){

        return 0.0;

    }

}


A ‘Circle’ class inherits from the ‘Shape’ class and overrides the ‘getArea()’ and ‘getPerimeter()’ methods because we have the formula to calculate the area and perimeter of a circle. 


public class Circle extends Shape {

    private double radius;

    public Circle(String name, double radius) {

        super(name);

        this.radius = radius;

    }

    @Override

    public double getArea() {

        return Math.PI * radius * radius;

    }

    @Override

    public double getPerimeter(){

        return 2*Math.PI*radius;

    }

}


On the other hand, the ‘Triangle’ class also inherits from the ‘Shape’ class. It overrides the ‘getArea()’ and ‘getPerimeter()’ methods to calculate properly the area and the perimeter of a triangle. 



public class Triangle extends Shape {

    private double line1;

    private double line2;

    private double line3;

    public Triangle(String name, double line1, double line2, double line3) {

        super(name);

        this.line1 = line1;

        this.line2 = line2;

        this.line3 = line3;

    }

    @Override

    public double getArea() {

        double s = (line1 + line2 + line3) / 2;

        return Math.sqrt(s * (s - line1) * (s - line2) * (s - line3));

    }

    @Override

    public double getPerimeter() {

        return line1 + line2 + line3;

    }

}


The code of the ‘Shape’ class wraps the common behavior shared by all shapes. The ‘Circle’ and ‘Triangle’ classes inherit this behavior because they have an area and a perimeter. We can say that the ‘Circle’ and ‘Triangle’ instances behave exactly like an instance of the ‘Shape’ super-class. Therefore, they can be substitutable for the ‘Shape’ super-class objects. Let’s illustrate this in an example of a demo class:


public class LSPDemo {

    public static void main(String[] args) {

        Shape circle = new Circle("Circle", 16.5);

        Shape triangle = new Triangle("Triangle", 10.0, 14.0, 18.0);

        List<Shape> shapesList = List.of(circle, triangle);

        double shapesTotalArea = getShapesTotalArea(shapesList);

        System.out.println("Shapes total area: " + shapesTotalArea);

    }


    public static double getShapesTotalArea(List<Shape> shapesList) {

        double shapesTotalArea = 

shapesList.stream().map(s->s.getArea()).reduce(Double::sum).get();

        return shapesTotalArea;

    }

}


In this example, we create two shapes as instances of the ‘Circle’ and ‘Triangle’ classes.  We put these two instances in the ‘shapesList’ list which does not distinguish between circles and triangles. 
We define a function ‘getShapesTotalArea’ which takes a list of objects of the ‘Shape’ class and calculates the total area of all shapes in the list. It uses the Java Stream API to map each ‘Shape’ object to its area using its ‘getArea’ method. Then, it sums them up and returns the total area.
This implementation adheres to the Liskov Substitution Principle. Objects of the ‘Circle’ and ‘Triangle’ sub-classes have successfully replaced objects of the ‘Shape’ super-class in the ‘getShapesTotalArea’ function. Executing the main function of the ‘LSPDemo’ class gives us the following result:


Shapes total area: 924.9477205372846


3. Example of LSP in Python

Suppose we have a class hierarchy that represents different species of animals. We have a base class ‘Animal’ and two sub-classes ‘Dog’ and ‘Fish’. To adhere to LSP, instances of ‘Dog’ and ‘Fish’ should be able to replace instances of ‘Animal’ without affecting the correctness of the program. Let’s inspect if this is the case in our example.


The base class ‘Animal’ has a ‘walk’ method supported by its sub-class ‘Dog’ but not by its sub-class ‘Fish’. In our example, we threw an exception to indicate that this operation is unsupported in the ‘Fish’ sub-class. In this case, an instance of the ‘Fish’ sub-class cannot substitute an object of the ‘Animal’ super-class. Thus, this example violates the Liskov Substitution Principle. 


To illustrate that, we define a function ‘animal_walking’ that takes an animal object as input and calls its ‘walk’ method. As fishes are animals but cannot walk, then the ‘walk’ method should not belong to the ‘Animal’ class but to a sub-class in the hierarchy to regroup only animals that have this feature.


class Animal:

    def __init__(self, weight):

        self.weight = weight

    def eat(self):

        print("I can eat")

    def walk(self):

        print("I can walk")


class Dog(Animal):

    def __init__(self, weight, no_legs):

        super().__init__(weight)

        self.no_legs = no_legs

    def bark(self):

        print("I can bark")


class Fish(Animal):

    def walk(self):

        raise Exception("Sorry, unsupported operation")

    def swim(self):

        print("I can swim")


def animal_walking(animal):

    animal.walk()



# Examples of using these classes

dog = Dog(10, 4)

animal_walking(dog)

fish = Fish(2)



# Violation of Liskov Substitution Principle

animal_walking(fish)


Running the above example gives us the following output:


I can walk

Traceback (most recent call last):

...

    raise Exception("Sorry, unsupported operation")

Exception: Sorry, unsupported operation

SOLID Principles: Single Responsibility Principle



In this series, I will highlight some areas where you can improve the quality of your code during the development process. In the first article, I explained why code quality is paramount. Then I introduced briefly some ideas that would help you improve the quality of your code. I will provide more details on these points in future articles. Some concepts will be illustrated with examples in Java and Python. I will begin this journey with you by introducing the SOLID principles in this article. They are some of the concepts that will help you improve the quality of your code and boost your abstract thinking in the object-oriented paradigm. I will elucidate the first principle (Single Responsibility Principle), with examples in Java and Python.


1. SOLID principles

The SOLID principles are a collection of object-oriented design concepts for building a solid object-oriented software architecture. These principles help developers create software that are more readable, flexible, extensible, and maintainable.


It was Robert C. Martin who first introduced these principles. But it was Michael Feathers, who came up with the acronym SOLID and reworked these principles. The acronym SOLID stands for:


    • S: Single Responsibility Principle

    • O: Open/closed Principle

    • L: Liskov Substitution Principle

    • I: Interface Segregation Principle

    • D: Dependency Inversion Principle


2. Advantages of Using SOLID Principles

Compliance with SOLID principles has a big impact on the quality of your software. We can mention some benefits of using these principles as follows:


    • Reduce the complexity of the code.

    • Increase readability, extensibility, and maintenance.

    • Increase flexibility and reusability.

    • Reduce tight coupling.

    • Reduce errors for better testability.


We’ll show how using SOLID principles helps you develop a code with better quality that confirms the achievement of the advantages described above. In this series, we will explain each principle individually and illustrate with an example how each improves certain aspects of the software. But, we focus only on the Single Responsibility Principle (SRP)  in this article.


3. Single Responsibility Principle (SRP)

This principle means that a class should only have one responsibility. In other words, each class should only contain one feature or behavior. This principle helps you ensure code quality by preserving some of the code quality features we mention via the following benefits:


Testability: It is easy to define test cases for a class with a single responsibility.

Loose coupling: A class implementing SRP has few dependencies because it only encapsulates a single functionality.

Readability: This principle ensures that classes adhering to it are small, well-organized, and easy to understand.

Separation of Concerns: This principle preserves the concept of separation of concerns.

Maintainability: The ability to update the software is high.

Extensibility: Adding new features is easy.


4. Example of SRP in Java

To explain how a class complies with SRP or not, we create an illustrative class ‘Calculator’ with a few methods that we named ‘add’, ‘div’, ‘isMultipleOf’, and ‘removeWSpace’.

We defined the first three methods for arithmetic operations and followed strong logic to put them together (strong cohesion). 

The fourth operation removes white spaces from a variable of type string. That means it has a weak cohesion with the ‘add’, ‘div’, and ‘isMultipleOf’. 


Consider one responsibility according to our requirements, which is achieving mathematical operations. Therefore, this example violates the SRP because it does not encapsulate this sole responsibility. This could result in the loss of the benefits of the SRP mentioned above.


public class Calculator {

    public Calculator() { }

    public static int add(int x, int y) {

        return x + y;

    }

    public static int div(int x, int y) {

        return x / y;

    }

    public static boolean isMultipleOf(int x, int y) {

        if (x % y == 0) {

            return true;

        } else {

            return false;

        }

    }

    public static String removeWSpace(String inputStr) {

        return inputStr.replaceAll("\\s+","");

    }

}


5. Example of SRP in Python

Consider a class in Python that violates the SRP according to our requirements. We define a class named ‘ListExtremum’ which encapsulates four methods ‘find_max’, ‘find_min’, ‘find_max_dict’, and ‘find_min_dict’. 

The ‘find_max’ and ‘find_min’ methods look for the max and the min values respectively in a list. The ‘find_max_dict’ and ‘find_min_dict’ methods return the max and the min values respectively in a dictionary.

This example violates the SRP because it fulfills more than one responsibility. The first responsibility consists of calculating the extrema (minimum and maximum) of a list. The second one is to calculate the extrema of a dictionary.


class ListExtremum:

    def __init__(self, num_list, dict):

        self.num_list = num_list

        self.dict = dict


    def find_max(self):

        try:

            maximum = self.num_list[0]

            for number in self.num_list:

                if number > maximum:

                    maximum = number

            return maximum

        except TypeError:

            print("Method find_max - Error empty list")

        except IndexError:

            print("Method find_max - Error empty list")


    def find_min(self):

        try:

            minimum = self.num_list[0]

            for number in self.num_list:

                if number < minimum:

                    minimum = number

            return minimum

        except TypeError:

            print("Method find_min - Error empty list")

        except IndexError:

            print("Method find_min - Error empty list")


    def find_max_dict(self):

        try:

            max_key = max(self.dict, key=self.dict.get)

            max_value = self.dics[max_key]

            return max_value

        except AttributeError:

            print("Method find_max_dict - Error empty dictionary")

        except ValueError:

            print("Method find_max_dict - Error empty dictionary")


    def find_min_dict(self):

        try:

            min_key = min(self.dics, key=self.dics.get)

            min_value = self.dics[min_key]

            return min_value

        except AttributeError:

            print("Method find_min_dict - Error empty dictionary")

        except ValueError:

            print("Method find_min_dict - Error empty dictionary")




We explain what each method does in this code.
Constructor ‘__init__’: This code defines a constructor function (__init__) for the ‘ListExtremum’ class. It takes two arguments (‘num_list’ and ‘dict’) and assigns them to instance variables (‘self.num_list’ and ‘self.dict’).
Method ‘find_max’: this method searches for the maximum number in ‘self.num_list’. It initializes the maximum variable with the first element of ‘self.num_list’. Then, it iterates over the list and updates the maximum variable when it finds a number greater than the current maximum. It handles ‘TypeError’ and ‘IndexError’ exceptions and prints an error message if the list is None or empty.
Method ‘find_min’: This method returns the minimum value in ‘self.num_list’. It initializes the minimum variable with the element of ‘self.num_list’ at the first index. After that, it iterates over the list and updates the minimum variable when it finds a number smaller than the current minimum. It handles potential errors for an empty list by catching ‘TypeError’ and ‘IndexError’ exceptions and printing an error message.
Method ‘find_max_dict’: This method finds the key-value pair with the highest value in a dictionary. It uses the max function with the key parameter set to ‘self.dict.get’ to find the key with the highest value. If the dictionary is empty, it catches the ‘AttributeError’ and ‘ValueError’ exceptions and prints an error message.
Method ‘find_min_dict’: This method aims to find the key-value pair with the minimum value in a dictionary. It uses the min function to find the minimum key based on the values. Then, it retrieves the corresponding value from the dictionary. Like ‘find_max_dict’, this method catches the ‘AttributeError’ and ‘ValueError’ exceptions and prints an error message, if the dictionary is empty.
We should split this code into parts to make this class adhere to the SRP. The first part will implement operations applied to a list. The second part contains the operations of dictionaries. In this case, we get two classes called ‘ListExtremum’ and ‘DictExtremum’. 
The ‘ListExtremum’ class contains the ‘find_max’ and ‘find_min’ methods to calculate the max and min value of a list. On the other hand, the ‘DictExtremum’ class contains the ‘find_max_dict’ and ‘find_min_dict’ methods to get the max and min values of a dictionary.
class ListExtremum:
    def __init__(self, num_list):
        self.num_list = num_list
    def find_max(self):
        try:
            maximum = self.num_list[0]
            for number in self.num_list:
                if number > maximum:
                    maximum = number
            return maximum
        except TypeError:
            print("Method find_max - Error empty list")
        except IndexError:
            print("Method find_max - Error empty list")
    def find_min(self):
        try:
            minimum = self.num_list[0]
            for number in self.num_list:
                if number < minimum:
                    minimum = number
            return minimum
        except TypeError:
            print("Method find_min - Error empty list")
        except IndexError:
            print("Method find_min - Error empty list")

class DictExtremum:
    def __init__(self,  dict):
        self.dict = dict
    def find_max_dict(self):
        try:
            max_key = max(self.dict, key=self.dict.get)
            max_value = self.dict[max_key]
            return max_value
        except AttributeError:
            print("Method find_max_dict - Error empty dictionary")
        except ValueError:
            print("Method find_max_dict - Error empty dictionary")
    def find_min_dict(self):
        try:
            max_key = min(self.dics, key=self.dics.get)
            max_value = self.dics[max_key]
            return max_value
        except AttributeError:
            print("Method find_min_dict - Error empty dictionary")
        except ValueError:
            print("Method find_min_dict - Error empty dictionary")

We can call this code using some examples as follows:
print("**********************************************")
print("Calculate max and min values in None list and None dictionary")
print("")
list_extremum_calc = ListExtremum(None)
print("Max value in the list: ", list_extremum_calc.find_max())
print("Min value in the list: ", list_extremum_calc.find_min())
dict_extremum_calc = DictExtremum(None)
print("Max value in the dictionary: ", dict_extremum_calc.find_max_dict())
print("Min value in the dictionary: ", dict_extremum_calc.find_min_dict())
print("")
print("**********************************************")
print("Calculate max and min values in empty list and empty dictionary")
list_extremum_calc = ListExtremum([])
print("Max value in the list: ", list_extremum_calc.find_max())
print("Min value in the list: ", list_extremum_calc.find_min())
dict_extremum_calc = DictExtremum({})
print("Max value in the dictionary: ", dict_extremum_calc.find_max_dict())
print("Min value in the dictionary: ", dict_extremum_calc.find_min_dict())
print("")
print("**********************************************")
num_list = [18, 20, 25, -16, 27]
temp_dict = {'Vienna': 10, 'Paris': 11, 'Rome': 15, 'Algiers': 16}
print("num_list: ", num_list)
print("temp_dict: ", temp_dict)
print("Calculate max and min values in num_list and temp_dict")
print("")
list_extremum_calc = ListExtremum(num_list)
print("Max value in the list: ", list_extremum_calc.find_max())
print("Min value in the list: ", list_extremum_calc.find_min())
dict_extremum_calc = DictExtremum(temp_dict)
print("Max value in the dictionary: ", dict_extremum_calc.find_max_dict())
print("Min value in the dictionary: ", dict_extremum_calc.find_min_dict())

The execution of this code gives us the following result:
**********************************************
Calculate max and min values in None list and None dictionary
Method find_max - Error empty list
Max value in the list:  None
Method find_min - Error empty list
Min value in the list:  None
Method find_max_dict - Error empty dictionary
Max value in the dictionary:  None
Method find_min_dict - Error empty dictionary
Min value in the dictionary:  None
**********************************************
Calculate max and min values in empty list and empty dictionary
Method find_max - Error empty list
Max value in the list:  None
Method find_min - Error empty list
Min value in the list:  None
Method find_max_dict - Error empty dictionary
Max value in the dictionary:  None
Method find_min_dict - Error empty dictionary
Min value in the dictionary:  None
**********************************************
num_list:  [18, 20, 25, -16, 27]
temp_dict:  {'Vienna': 10, 'Paris': 11, 'Rome': 15, 'Algiers': 16}
Calculate max and min values in num_list and temp_dict
Max value in the list:  27
Min value in the list:  -16
Max value in the dictionary:  16
Method find_min_dict - Error empty dictionary
Min value in the dictionary:  None

Blog Posts

Enhancing Performance of Java-Web Applications

Applications built with a Java back-end, a relational database (such as Oracle or MySQL), and a JavaScript-based front-end form a common and...