LSP - The Liskov Substitution Principle

Subscribe to my newsletter and never miss my upcoming articles

The Liskov Substitution Principle is a part of SOLID, a mnemonic acronym that bundles a total of 5 design principles.

It is often associated with clean code.

But what exactly is it, is it important to you, and should you even care?

What does it state?

If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program (correctness, task performed, etc.).

Sounds complicated, right?

Well, it can be boiled down to a simpler definition:

Software (systems) should be built from interchangeable parts. Those parts should agree on a common contract, which enables those parts to be substituted one for another.

Still maybe not that easy, but let's go one step further, and take a look at it from the perspective of JavaScript:

✅ Methods of a subclass that override methods of a base class must have exactly the same number of arguments

✅ Each argument of the overriding method must have the same type as in the method of the base class

✅ The return type of a method overriding a base method must be of the same type

✅ It must only throw the same types of exceptions that the base class does

Some examples

To get an even better understanding of what exactly LSP is all about, you can take a look at the following examples, which illustrate violations of the principle.

Example 1

This example violates the LSP because the subclass FriendlyGreeter adds another parameter to the greet method.

class Greeter {
  greet(name) {
    return `Hello, ${name}!`;
  }
}

class FriendlyGreeter extends Greeter {
  greet(name, age) {
    return `Hello, ${age} year old ${name}, have a nice day!`;
  }
}

Example 2

In this example, the subclass Penguin throws an error that isn't known by the base class and which is also unknown to users of the Bird class.

Although this issue is one of the less obvious ones, it's actually a common violation of the LSP.

class Bird {
  fly() {
    return "I'm flying!";
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error("I wish I could but I can't. :(");
  }
}

What does it try to prevent?

The principle tries to protect you and your users from surprising behavior.

And do you know what this leads to? => You or library creators can create base logic, and rely on a contract.

This contract specifies the parameters, the return type, and what errors might be thrown. And with all of this at hand, you can implement logic that actually handles exactly this => Nothing more, nothing less.

If someone that writes base logic specifies that a CannotDoError can be thrown, only this error has to be handled when using that logic or deriving from it, and not the multitude of errors that anyone could come up with, next to that one.

As long as users agree on the contract, they won't have any problems with unexpected behavior from your side. And this is pretty awesome because your software works as expected.

Should you care?

Oh yes, you should.

Building reliable and extendable software that doesn't surprise anyone should be your major goal.

All of this leads to development speed, and speed leads to features being shipped more frequently.

Before you leave

If you like my content, visit me on Twitter, and perhaps you’ll like what you see!

No Comments Yet