Design Principle: Liskov Substitution Principle(LSP)

Summary

Principle NameLiskov Substitution Principle
AcronymLSP
Other NamesPrinciple of type conformance(or substitutability)
Principle Type SOLID
TaglineSubtypes must be substitutable for their base types.
Implementing
Design Patterns
Adapter Pattern
Command Pattern
Composite Pattern
Decorator Pattern
Factory Method Pattern
Observer Pattern
Proxy Pattern
State Pattern
Strategy Pattern
Template Method Pattern
Related Principles Single Responsibility Principle(SRP)
Open/Closed Principle(OCP)
Interface Segregation Principle(ISP)
Dependency Inversion Principle(DIP)

Definition

Liskov Substitution Principle(LSP) is the third(3rd) principle of the SOLID Principles.

This principle instructs how subclasses should relate to the superclasses.

Liskov Substitution Principle(LSP) says-

Objects of superclass should be replaceable by the objects of a subclass, without affecting the correctness of the program.

When subclass object replaces super class object, the result might not be exactly same, but rather it –

  • should be consistent
  • has the same intended behavior
  • should not break any functionality

This way we can introduce new subclasses, without any risk of breaking existing functionality.

This principle is important for writing robust and maintainable code. As it ensures that a subclass will not behave unexpectedly, compared to the superclass.

What You Should Do

Maintain Base Class Contract: make sure that the subclass maintains the contract promised by the base class. i.e. if a base class method returns some kind of sum, then the child class method should also return a sum.
Override Behavior Consistently: we can override methods but the overridden methods should have similar behavior to the parent class method.
Work as Replacement: if a child class object is passed to a function in place of a parent class object, then it should work similarly(not exactly the same, but it should have similar output/behavior).

What You Cannot Do

Do not Weaken Output: subclass must not provide weaker output. i.e. if the base class returns an integer or float, the subclass should not suddenly return
Do not Strengthen Input: subclass should accept a wider(or the same) range of input. i.e. if a superclass method accepts an integer or float, then we should not start accepting only positive integers in the subclass method.
No Unexpected Side Effects: if the superclass method does not have any side effects, then we should not introduce an unexpected side effect in the subclass method. (i.e. some state changes)
No Change in Interface Contract: subclass should not change the behavior or meaning of the methods, to something that is incompatible with the base class.

Implementation

Here we have a base class Shape, and some classes extending Shape.

The Shape declares a function for calculating area. So in the subclasses in the same function definition, we should implement the calculation for calculating area(and nothing else).

This way the subclasses, maintain the same behavior as the parent class.

import java.lang.Math;

abstract class Shape {
    abstract double getArea();
}

class Rectangle extends Shape {
    private double width;
    private double height;

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

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

class Triangle extends Shape {
    private double side1, side2, side3;

    public Triangle(double side1, double side2, double side3) {
        this.side1 = side1;
        this.side2 = side2;
        this.side3 = side3;
    }

    @Override
    public double getArea() {
        double s = (side1 + side2 + side3) / 2;
        return Math.sqrt(s * (s - side1) * (s - side2) * (s - side3));
    }
}

class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * Math.pow(radius, 2);
    }
}

public class Main {
    public static void printArea(Shape shape) {
        System.out.println("The area is: " + shape.getArea());
    }

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(5, 10);
        Triangle triangle = new Triangle(3, 4, 5);
        Circle circle = new Circle(7);

        printArea(rectangle);
        printArea(triangle);
        printArea(circle);
    }
}
Java
from abc import ABC, abstractmethod
import math


class Shape(ABC):
    @abstractmethod
    def get_area(self) -> float:
        """Calculate the area of the shape"""
        pass


class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def get_area(self) -> float:
        return self.width * self.height


class Triangle(Shape):
    def __init__(self, side1: float, side2: float, side3: float) -> None:
        self.side1 = side1
        self.side2 = side2
        self.side3 = side3

    def get_area(self) -> float:
        # Calculate the semi-perimeter
        s = (self.side1 + self.side2 + self.side3) / 2
        # Apply Heron's formula
        area = math.sqrt(s * (s - self.side1) * (s - self.side2) * (s - self.side3))
        return area


class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def get_area(self) -> None:
        return math.pi * self.radius**2


def print_area(shape: Shape):
    print(f"The area is: {shape.get_area()}")


# Demo usage

# Instances of Rectangle and Circle
rectangle = Rectangle(5, 10)
triangle = Triangle(3, 4, 5)
circle = Circle(7)

# Calling print_area() with both subclasses
print_area(rectangle)
print_area(triangle)
print_area(circle)
Python
package main

import (
	"fmt"
	"math"
)

type Shape interface {
	GetArea() float64
}

type Rectangle struct {
	width, height float64
}

func (r Rectangle) GetArea() float64 {
	return r.width * r.height
}

type Triangle struct {
	side1, side2, side3 float64
}

func (t Triangle) GetArea() float64 {
	s := (t.side1 + t.side2 + t.side3) / 2
	return math.Sqrt(s * (s - t.side1) * (s - t.side2) * (s - t.side3))
}

type Circle struct {
	radius float64
}

func (c Circle) GetArea() float64 {
	return math.Pi * math.Pow(c.radius, 2)
}

func printArea(shape Shape) {
	fmt.Printf("The area is: %.2f\n", shape.GetArea())
}

func main() {
	rectangle := Rectangle{width: 5, height: 10}
	triangle := Triangle{side1: 3, side2: 4, side3: 5}
	circle := Circle{radius: 7}

	printArea(rectangle)
	printArea(triangle)
	printArea(circle)
}
Go
<?php

abstract class Shape
{
    abstract public function getArea(): float;
}

class Rectangle extends Shape
{
    private float $width;
    private float $height;

    public function __construct(float $width, float $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function getArea(): float
    {
        return $this->width * $this->height;
    }
}

class Triangle extends Shape
{
    private float $side1;
    private float $side2;
    private float $side3;

    public function __construct(float $side1, float $side2, float $side3)
    {
        $this->side1 = $side1;
        $this->side2 = $side2;
        $this->side3 = $side3;
    }

    public function getArea(): float
    {
        $s = ($this->side1 + $this->side2 + $this->side3) / 2;
        return sqrt($s * ($s - $this->side1) * ($s - $this->side2) * ($s - $this->side3));
    }
}

class Circle extends Shape
{
    private float $radius;

    public function __construct(float $radius)
    {
        $this->radius = $radius;
    }

    public function getArea(): float
    {
        return pi() * pow($this->radius, 2);
    }
}

function printArea(Shape $shape)
{
    echo "The area is: " . $shape->getArea() . PHP_EOL;
}

# Demo usage
$rectangle = new Rectangle(5, 10);
$triangle = new Triangle(3, 4, 5);
$circle = new Circle(7);

printArea($rectangle);
printArea($triangle);
printArea($circle);
PHP
abstract class Shape {
    abstract getArea(): number;
}

class Rectangle extends Shape {
    private width: number;
    private height: number;

    constructor(width: number, height: number) {
        super();
        this.width = width;
        this.height = height;
    }

    getArea(): number {
        return this.width * this.height;
    }
}

class Triangle extends Shape {
    private side1: number;
    private side2: number;
    private side3: number;

    constructor(side1: number, side2: number, side3: number) {
        super();
        this.side1 = side1;
        this.side2 = side2;
        this.side3 = side3;
    }

    getArea(): number {
        const s = (this.side1 + this.side2 + this.side3) / 2;
        return Math.sqrt(s * (s - this.side1) * (s - this.side2) * (s - this.side3));
    }
}

class Circle extends Shape {
    private radius: number;

    constructor(radius: number) {
        super();
        this.radius = radius;
    }

    getArea(): number {
        return Math.PI * Math.pow(this.radius, 2);
    }
}

function printArea(shape: Shape): void {
    console.log(`The area is: ${shape.getArea()}`);
}

const rectangle = new Rectangle(5, 10);
const triangle = new Triangle(3, 4, 5);
const circle = new Circle(7);

printArea(rectangle);
printArea(triangle);
printArea(circle);
TypeScript

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.