Shahmir Faisal
Shahmir Faisal

Shahmir Faisal

Liskov Substitution Principle - SOLID Principles in Javascript

Liskov Substitution Principle - SOLID Principles in Javascript

Learn about the Lisov Substitution Design Principle to write clean code.

Shahmir Faisal's photo
Shahmir Faisal
·Aug 13, 2022·

4 min read

Subscribe to my newsletter and never miss my upcoming articles

Play this article

Table of contents

  • What is Liskov Substitution Principle?
  • Example

This is the 3rd article in our series of articles covering the SOLID Design Principles.

If you are unaware of SOLID, you can read the first article where I gave a brief introduction to SOLID. In this article, we will be covering the Liskov Substitution Principle.

What is Liskov Substitution Principle?

It states that an object of the parent class should be replaceable with the object of the child class without breaking the application.

You might have heard this definition in some other places or maybe it's your first time but anyways, it sounds confusing.

In simple words, if you have a parent and a child class and you are using an object of parent class somewhere in your code. If you replace that object of the parent class with the object of the child class, everything should still work the same without giving any errors or unexpected output.

Example

Let's clear this concept with the help of an example.

Consider we have a Rectangle class as a parent and a Square class as the child.

Naive Solution

class Rectangle {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }

  set width(value) {
    this._width = value;
  }
  set height(value) {
    this._height = value;
  }

  get width() {
    return this._width;
  }
  get height() {
    return this._height;
  }

  get area() {
    return this._width * this._height;
  }
}

class Square extends Rectangle {
  constructor(width, height) {
    if (width !== height) {
      throw new Error("Width and Height should be same!");
    }

    super(width, height);
  }

  set width(value) {
    this._width = value;
    this._height = value;
  }
  set height(value) {
    this._width = value;
    this._height = value;
  }
}

Here we have 2 properties _width and _height and we are using getters and setters for accessing and modifying the values. Now in the Square class, we are overriding the setters because we want to have the same width and height.

Now if we try to use them:

const calculateObjectArea = (object) => {
  object.height = 20;
  return object.area;
};

const rc = new Rectangle(10, 10);

console.log(calculateObjectArea(rc));
// Output: 200

Here, we have a function calculateObjectArea that changes the height to 20 and then calculates the area. So basically in our case, the output will be 200 (10 * 20).

Now, what if we pass a square to that function instead of a rectangle with the same width and height?

const calculateObjectArea = (object) => {
  object.height = 20;
  return object.area;
};

const rc = new Rectangle(10, 10);
const sq = new Square(10, 10);

console.log(calculateObjectArea(sq));
// Output: 400

According to the principle, if we replace the object of the parent class with the object of the child class we should still get the expected output. But here we will get 400 instead of 200. It means we are violating the Liskov Substitution Principle.

Better Solution

class Shape {
  get area() {
    return this._width * this._height;
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this._width = width;
    this._height = height;
  }

  get width() {
    return this._width;
  }
  get height() {
    return this._height;
  }

  set width(value) {
    this._width = value;
  }
  set height(value) {
    this._height = value;
  }
}

class Square extends Shape {
  constructor(width, height) {
    if (width !== height) {
      throw new Error("Width and Height should be same!");
    }
    super();
    this._width = width;
    this._height = height;
  }

  get width() {
    return this._width;
  }
  get height() {
    return this._height;
  }

  set width(value) {
    this._width = value;
    this._height = value;
  }
  set height(value) {
    this._width = value;
    this._height = value;
  }
}

Here we put all the common things in one base class Shape (in our case it is just the area). And then in Rectangle and Square we are having different setters and getters. Now the Square class is completely independent of the Rectangle class.

So whenever you are using inheritance and when you replace one object with another one and your code behaves unexpectedly, try to split your code into multiple classes and put only common things in the base class.

So that's it. Comment below if you have any questions. Will see you in the next article covering the Interface Segregation Principle.

Follow me on:
Twitter
LinkedIn
Github

 
Share this