SOLID - Liskov Substitution Principle

Solid Principles

In this tutorial we are going to learn about the Liskov Substitution Principle (LSP).

Table of contents

GitHub repository

Reference

  • S Single Responsibility Principle
  • O Open-Closed Principle
  • L Liskov Substitution Principle
  • I Interface Segregation Principle
  • D Dependency Inversion Principle

Definition

Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. Violating this principle can lead to unexpected behavior and errors.

Rules for the methods of the subclasses

  • Subclass must not weaken post-condition and must not strengthen pre-condition.
  • Exception types thrown by the method of the subclass must be same as the superclass or it must be subtypes of the method of the superclass.
  • The parameter types of the method of the subclass must be same as the superclass or it must be more abstract than superclass.
  • The return type of the method of the subclass must be same as the superclass or it must be a subtype of the method of the superclass.

Pre-condition

It is a condition that must be true before a function (method) is executed. So, by pre-condition a method tells the client that it is expecting something to be in place before it can execute.

In the following example we have the add method that takes two integer values. So, the pre-condition is that the two values passed must be valid integer values. If they are not then we will get error and the method will not execute.

class MathOps {
  public int add(int x, int y) {
    return x + y;
  }
}


class MyClass {
  public static void main(String[] args) {
    MathOps obj = new MathOps();
    System.out.println(obj.add(1, "A"));  // error: incompatible types: String cannot be converted to int
  }
}

Post-condition

It is a condition that will be true after a function (method) is executed and if the pre-condition is true. So, by post-condition a method tells the client that it promises to do something provided the pre-condition is true and the method has completed its execution.

In the following example we have the add method that takes two integer values. The post-condition for this method is that it will always return an integer value when the add method completes its exectution provided the pre-condition is true i.e., the two arguments passed to the add method are also valid integer values.

class MathOps {
  public int add(int x, int y) {
    return x + y;
  }
}


class MyClass {
  public static void main(String[] args) {
    MathOps obj = new MathOps();
    System.out.println(obj.add(1, 2));    // 3
  }
}

Example

In the following JAVA program we have the Square class that extends the Rectangle class.

class Rectangle {
    protected int width;
    protected int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}



class Square extends Rectangle {
    public Square(int side) {
        super(side, side);
    }

    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

Test

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class LSPTests {
    @Test
    @DisplayName("This will break LSP")
    public void shouldBreakLSP() {
        Rectangle rectangle = new Square(10);
        rectangle.setHeight(10);
        rectangle.setWidth(20);
        Assertions.assertNotEquals(rectangle.getArea(), 200);
    }
}

Problem

In the Square class we are overriding the setHeight and setWidth methods. So, setting the height of a Square also changes the width. Due to this the Square class violates the Liskov Substitution Principle as we can no longer use the Square object in place of the Rectangle.

Solution

In order to fix this problem we have to remove the "is-a" relationship between Square and Rectangle classes. A better solution is to create an interface Shape and both Square and Rectangle can implement it.

interface Shape {
    public double getArea();
}


class Square implements Shape {
    protected int side;

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

    public int getSide() {
        return side;
    }

    public void setSide(int side) {
        this.side = side;
    }

    @Override
    public double getArea() {
        return side * side;
    }
}


class Rectangle implements Shape {
    protected int width;
    protected int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }
}

Test

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class LSPTests {
    @Test
    @DisplayName("This will not break LSP")
    public void shouldNotBreakLSP() {
        Shape rectangle = new Rectangle(10, 20);
        Shape square = new Square(10);
        Assertions.assertEquals(rectangle.getArea(), 200);
        Assertions.assertEquals(square.getArea(), 100);
    }
}