Liskov Substitution Principle Isn’t Complex. Just Give It A Try
Tags: programming, designpatterns, solid, tutorial
Published: 2023-01-22 16:04:32

## Introduction

As we all know, software requirements always change, and we as developers need to make sure that these changes don't break the existing code. For this reason, the SOLID principles were introduced in Object-Oriented design to ease this process.

The SOLID principles are a set of principles set by [**Robert C. Martin**](https://en.wikipedia.org/wiki/Robert_C._Martin) (Uncle Bob). These principles help us create more flexible, maintainable, and understandable software. These principles are:

- [**S**ingle Responsibility Principle](https://dev.to/mayallo/do-you-really-know-what-the-single-responsibility-principle-is-3b4j)

- [**O**pen-Closed Principle](https://dev.to/mayallo/open-closed-principle-the-hard-parts-501c)

- **L**iskov Substitution Principle

- **I**nterface Segregation Principle

- **D**ependency Inversion

After introducing the [**O**pen-Closed Principle](https://dev.to/mayallo/open-closed-principle-the-hard-parts-501c) in the previous article, in this article, we will discuss the third principle **L**iskov Substitution Principle (LSP) which is the L in the SOLID acronym.

## Definition

Lets introduce the mathematical definition of the LSP and then jump into the details. The mathematical definition is introduced by Barbara Liskov in 1988:

> _If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T._

The basic object-oriented design controls the relationship between objects using either [Inheritance](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)) or [Composition](https://en.wikipedia.org/wiki/Composition_over_inheritance). Inheritance, the **IS-A** relationship, occurs when something **IS A** kind of another thing. For example, a horse **IS AN** animal. On the other hand, Composition, the **HAS-A** relationship with something else. For example, an address **HAS A** city related to it. The LSP brings additional constraint to the Object-Oriented Design and states that these relationships aren't sufficient and should be replaced with **IS-SUBSTITUTABLE-FOR**.

But what that means? Simply, it means that a supertype should be replaceable with its subtype without breaking the existing code. In other words, the supertype should behave the same as its subtype.

Having said that, how can we guarantee that replacing a supertype with a subtype has no side effects on our existing code?

## LSP extends OCP

Keep in mind that the five SOLID principles are somehow related to each other. And following one principle does not ensure that you are following the others correctly.

As we will see, **the LSP is extending the** [**Open-Closed principle**](https://dev.to/mayallo/open-closed-principle-the-hard-parts-501c), **and following the rules of the OCP isn't enough to ensure that your code is _open for extension and closed for modifications_**. **But your code as well has to conform to the Liskov Substitution Principle to avoid any side effects**.

Below is an example that will help you better understand this point:

![Follows OCP, not LSP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k32m6ylidpjml7zul6in.png)

As you see, this example is following the OCP in a perfect way. That is, if we want to add a new employee role, all we have to do is add a new class including the new role functionality that conforms to the `IEmployee` contract.

That sounds good. But let me ask you a question, how would you build the `Guest` role? yes, the guest role can `listPosts` but how would you deal with the `login` functionality as there is no authentication for a guest employee?

I think you could respond, _I can leave it blank with no functionality at all, or I can throw an unsupported exception in the_ `login` method. Yes, these solutions are intuitive if you don't keep in mind the LSP.

Again, you could ask me, _since I have fulfilled the OCP perfectly and that's the most important thing to me, then what's the problem if I violate the LSP?_ I think this question has an implicit correct point which is some principles are more important than others. However that said, we should not ignore a principle because it is less important.

As we already knew, **the LSP is all about substitution** between a supertype and a subtype without impacting the existing client code. Keep in mind this point and lets jump again into your solutions:

- _leaving it blank with no functionality at all:_ Now, your client code expects the `login` function to return an authenticated user token, what will happen if you used the `Guest.login` that returns nothing? Logically, it will break your existing code, wont it?

- _throwing an unsupported exception:_ Again, your existing code doesn't handle this new exception from `Guest.login`. So, as a result, using the `Guest.login` will break the existing code.

**Surprisingly, this design perfectly follows the OCP, however, it violates the LSP.**

## The LSP Rules

![Liskov Substitution Principle Rules](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pwlflcaow84o3g7ph36c.jpg)

Unfortunately, there is no easy way to enforce this principle in your code. However, to apply this principle correctly in your code, there are two types of rules you have to follow, the **Signature rules and the** [**Behavior rules**](https://en.wikipedia.org/wiki/Behavioral_subtyping) **.**

Although you can enforce the **Signature rules** using a compiled language like Java, you can't enforce the **Behavior rules** , instead, you need to implement your own checks to enforce a specific behavior.

### Firstly, let's introduce the Signature rules:

**1-** [**Countervariance**](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)) **of Method Arguments** : It is a conversion from a more specific to a more general type. In other words, the overridden subtype method argument has to be the same as the supertype or wider.

```typescript
class SuperType {
   getItem(item: string) {}
 }

 class SubType extends SuperType {
   getItem(item: string | number) { // Wider input type
     if (typeof item === 'string') {}
     else if (typeof item === 'number') {}
   }
 }

 const ins = new SuperType(); // Try to replace the SuperType with SubType
 const stringInput = 'string';
 ins.getItem(stringInput);
```
If the client code expects a `string`-only argument in the `SuperType`, logically, if you replaced `SuperType` with `SubType` which accepts `string` or `number` arguments (wider), the client code will notice no differences.

**2-** [**Covariance**](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)) **of Return Types** : It is a conversion from a more general type to a more specific one. In other words, the return type of the overridden subtype method has to be the same as the supertype or narrower.

```typescript
class SuperType {
  getItem(): number | string {
    const anyCond = true;
    if (anyCond) {
      return 'String output';
    }
    return 5;
  }
}

class SubType extends SuperType {
  getItem(): string {
    return 'Only string output';
  }
}

const ins = new SuperType(); // Try to replace the SuperType with SubType
const res = ins.getItem();
if (typeof res === "string") {
  // Handle the string response
} else if (typeof res === "number") {
  // Handle the number response
}
```
The client code has already handled a `string` or `number` response coming from the `SuperType`. So if you replaced the `SuperType` with the `SubType` which returns only a `string` response, logically, the client code wouldn’t break.

**3- Exceptions** : The subtype method has to throw the same exceptions of the supertype or narrower. In fact, this rule couldn't be enforced by all compiled languages. There are some languages that can enforce it like Java, and others that couldn't enforce it like TypeScript.
```typescript
 class SuperType {
   getItem(itemId: number) {
     if (!itemId) throw new Error("NOT_FOUND");
     if (itemId <= 0) throw new Error("INVALID_INPUT");
   }
 }

 class SubType extends SuperType {
   getItem(itemId: number) {
     if (!itemId) throw new Error("NOT_FOUND"); // Narrower exceptions
   }
 }

 try {
   const ins = new SuperType(); // Try to replace the SuperType with SubType
   ins.getItem(-1);
 } catch (error: any) {
   if (error.message === "NOT_FOUND") {}
   else if (error.message === "INVALID_INPUT") {}
 }

```
Like the previous rule, as long as, the client code that depends on the `SuperType` handles more exceptions, if you replaced this `SuperType` with the `SubType` which handles fewer exceptions, the client code would notice no difference.

### Secondly, let's introduce the Behavior rules:

**1-** [**Class Invariants**](https://en.wikipedia.org/wiki/Class_invariant) **(Property Rule)**: The subtype methods have to preserve or strengthen the supertypes class invariants.
```typescript
 class SuperType {
   // Class invariant: dayHour >= 0 && dayHour <= 23
   protected dayHour: number = 12;
 }

 class SubType extends SuperType {
   // SubType should preserve or strengthen the SuperType's invariant
   setDayHour(hour: number) {
     if (hour < 0 || hour > 12) { // Strengthen the SuperType's invariant
       throw new Error('INVALID_INPUT');
     }
     this.dayHour = hour;
   }
 }

 const ins = new SubType();
 ins.setDayHour(5); // Valid. Preserve the SuperType's invariant (5 > 0 && 5 < 23)
 ins.setDayHour(30); // Invalid. Not preserve the SuperType's invariant (30 > 23)
```
The `SubType` has to maintain the same `SuperType` invariants or strengthen them. Think of it, if the `SubType` doesn’t maintain the same `SuperType` invariants, logically, it wouldn’t be substitutable for the `SuperType` and would break the client code that depends on a specific behavior from the `SuperType`.

**2-** **History Constraint (Property Rule)**: The subtype methods shouldn't allow a state change that the supertype doesn't allow.
```typescript
 class SuperType {
   // Class History
   // should be set once at the creation time
   // shouldn't be overridden after that
   protected itemId: number = 40;

   constructor(itemId: number) {
     this.itemId = itemId;
   }
 }

 class SubType extends SuperType {
   // Invalid method behavior
   setItem(itemId: number) {
     this.itemId = itemId;
   }
 }

 const ins = new SubType(2); // Valid to be set just at the creation time
 ins.setItem(5); // Invalid. Not preserve the SuperType class history (`itemId` value shouldn't be overridden)
```
Here, if the `SubType` ignored any constraint imposed by the `SuperType`, logically, this will break any client code that relies on these constraints. Therefore, `SubType` couldn't be substitutable for `SuperType`.

**3-** [**Preconditions**](https://en.wikipedia.org/wiki/Precondition) **(Method Rule)**: The subtype method should preserve or weaken the preconditions of the overridden supertype method. Here, if you weaken the condition, you relax its constraints.
```typescript
 class SuperType {
   protected dayHour: number = 10;

   setDayHour(hour: number) {
     if (hour < 0 || hour > 12) { // Precondition
       throw new Error('INVALID_INPUT');
     }
     this.dayHour = hour;
   }
 }

 class SubType extends SuperType {
   setDayHour(hour: number) {
     if (hour < 0 || hour > 23) { // Weakens the precondition
       throw new Error('INVALID_INPUT');
     }
     this.dayHour = hour;
   }
 }

 const ins = new SuperType(); // Try to replace the SuperType with SubType
 ins.setDayHour(10);
```
In the previous example, any client code that provides the `hour` input imposed to the `SuperType` condition `hour < 0 && hour > 12` will logically be imposed on a wider range `hour < 0 && hour > 23` from the `SubType`. In other words, `SubType` could be substitutable for `SuperType` without any side effects.

**4-** [**Postconditions**](https://en.wikipedia.org/wiki/Postcondition) **(Method Rule)**: The subtype method should preserve or strengthen the postconditions of the overridden supertype method.
```typescript
 class SuperType {
   sum(a: number, b: number) {
     const sum = a + b;
     if (sum > 50) { // Postcondition
       throw new Error('TOO_BIG')
     }
     return sum;
   }
 }

 class SubType extends SuperType {
   sum(a: number, b: number) {
     const sum = (a + b) * 0.5;
     if (sum > 30) { // Strengthens the postcondition
       throw new Error('TOO_BIG');
     }
     return sum;
   }
 }

 const ins = new SuperType(); // Try to replace the SuperType with SubType
 ins.sum(20, 40);
```
Like the previous example, if the client code expects the returned value from `SuperType` to have a maximum value of `50` will consequently be valid if you replaced it with a `SubType` returns a value with a maximum value of `30`.

## Don't be Confused like me

![Liskov Substitution Principle and Inheritance Confusion]
(https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v98f5vdwjqmjp2nscwwr.jpg)

At first glance, you might think that the Liskov Substitution Principle is all about Inheritance, but this is not true. I preferred to dedicate a separate section for this point to stress it more because it was confusing me a lot while learning this principle. I thought that the LSP could be applied only if I used Inheritance.

In fact, **the Liskov Substitution Principle has nothing to do with Inheritance**. **The LSP is just about Subtyping** regardless this subtyping comes from Inheritance or Composition\*.\* Since the LSP has nothing to do with inheritance, whether or not you use inheritance is irrelevant to whether or not the LSP applies\*.\* Take a look at this solution on [StackExchange](https://softwareengineering.stackexchange.com/q/432838).

So, don't tightly couple the two concepts, the LSP and Inheritance. Instead, just keep in mind the LSP if you are forced to use inheritance. Take a look again at the example of the **LSP extends OCP** section.

## LSP Violation

![Liskov Substitution Principle Violation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dt207jgfq89xbyklztkr.jpg)

Lets introduce the most common violations of LSP and try to redesign it to follow the LSP.

### Type Checking:

If you're checking the type of a variable inside a polymorphic code. Have a look at the below example:
```typescript
 interface IEmployee {}
 class Employee implements IEmployee {}
 class Guest implements IEmployee {}

 const employees: IEmployee[] = [];
 for (const employee of employees) {
   if (employee instanceof Guest) { // Checking the the employee type
     doSomethingSpecificForGuest();
   } else {
     doAnotherThingForOtherTypes();
   }
 }
```
As you see, this loop has two different functionalities based on the employee type. But what is the problem with this implementation?

Think again, the first problem here is that any time you work with employees, you might have to perform a check to see if this employee is a `Guest` type to do a specific functionality or another type to do another functionality.

The second problem, you might add new types in the future and you might have to visit everywhere this check exists to add specific behaviors to support these new types. On top of that, this obviously violates the [Open-Closed Principle](https://dev.to/mayallo/open-closed-principle-the-hard-parts-501c).

So how can we solve this problem? One solution is using the [Tell, Don't Ask Principle](https://martinfowler.com/bliki/TellDontAsk.html) or [Encapsulation](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)). It means, **don’t ask an instance for its type and then conditionally perform a specific action, instead, encapsulate that logic in the type itself and tell it to perform an action.**
Let’s apply this to the previous example:

```typescript
 interface IEmployee {
   doSomething: () => void;
 }
 class Employee implements IEmployee {
   doSomething() {}
 }
 class Guest implements IEmployee {
   doSomething() {}
 }

 const employees: IEmployee[] = [];
 for (const employee of employees) {
   employee.doSomething(); // Don't ask for the type, encapsulate the behavior, then tell the instance to do an action
 }
```
### Null Checking:

It has the same behavior as type checking. Have a look at the below example, instead of checking on `Guest` type, you are checking on `null` value like this `if (employee === null)`. Obviously, this violates the LSP as well.
```typescript
 type User = {
   id: number;
   name: string;
 };

 interface IEmployee {
   getUser: () => User;
 }
 class Employee implements IEmployee {
   getUser(): User {
     return { id: 1, name: "test" };
   }
 }

 const employees: IEmployee[] = [new Employee(), null, null, new Employee()];
 for (const employee of employees) {
   let user = null;
   if (employee === null) { // Null checking to do specific behavior
     user = { id: -1, name: "Not exist user" };
   } else {
     user = employee.getUser();
   }
   console.log(user);
 }
```
But how can solve this problem? One common solution for this problem is using the [Null Object design pattern](https://en.wikipedia.org/wiki/Null_object_pattern). Have a look at this redesign:

```typescript
 type User = {
   id: number;
   name: string;
 };

 interface IEmployee {
   getUser: () => User;
 }
 class Employee implements IEmployee {
   getUser(): User {
     return { id: 1, name: "test" };
   }
 }
 class NullEmployee implements IEmployee {
   getUser(): User {
     return { id: -1, name: "Not exist user" };
   }
 }

 // Factory function to guarantee that the IEmployee type would be returned all the time even if it was null
 function employeeFactroy(employee: IEmployee) {
   if (employee === null) return new NullEmployee();
   return employee;
 }

 const employees: IEmployee[] = [new Employee(), null, null, new Employee()];
 for (const employee of employees) {
   const emp = employeeFactroy(employee);
   const user = emp.getUser();
   console.log(user);
 }
```

### Throwing Not Implemented Exception:

This is a common one because of the partial implementation of an interface or a base class. Take a look again at the example of the “**LSP extends OCP**” section, you have to throw a **Not Implemented Exception** in the method `login` of the `Guest` subtype because it can’t fully implement the `IEmployee` interface (supertype).

The solution for this problem is to make sure to fully implement the supertype whether it was an interface or a base class.

However, you might argue that it is sometimes difficult to fully implement the interface, like our example. That’s true, if you have such a case, you probably need to double-check the relation between the supertype and subtype, the subtype might not be qualified to be a subtype for this supertype, in other words, **this is probably a violation for the Interface Segregation Principle**.

## Conclusion

In this article, we have just introduced the Liskov Substitution Principle. We knew that LSP adds a new constraint to the object-oriented design, it states that relationships are insufficient and you need to make sure that subtypes are substitutable for supertypes.

We also knew the rules that you need to follow to apply this principle correctly. And these rules could be categorized under Signature and Behavior rules.

After that, we introduced some common violations of this principle and solutions for them.

## Before you leave

Thanks a lot for staying with me up till this point, I hope you enjoy reading this article.

If you found this article useful, check out these articles as well:

- [Do you really know, what the Single Responsibility Principle is?](https://dev.to/mayallo/do-you-really-know-what-the-single-responsibility-principle-is-3b4j)

- [Open-Closed Principle: The Hard Parts](https://dev.to/mayallo/open-closed-principle-the-hard-parts-501c)

- [What is the difference between Strategy, State, and Template design patterns?](https://dev.to/mayallo/what-is-the-difference-between-strategy-state-and-template-design-patterns-513g)

- [4 Ways to Handle Async Operations in Javascript](https://dev.to/mayallo/4-ways-to-handle-async-operations-in-javascript-4n1l)

![Liskov Substitution Principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5vqpuhira0pwefonb7s.jpg)

## References

- [SOLID Principles for C# Developers](https://app.pluralsight.com/courses/c0ed5511-3776-457e-b06a-259fbb2144f7/table-of-contents)

- [Liskov Substitution Principle in Java](https://www.baeldung.com/java-liskov-substitution-principle)

- [SOLID Design Principles Explained: The Liskov Substitution Principle with Code Examples](https://stackify.com/solid-design-liskov-substitution-principle/)

- [Confusion about strengthening/weakening preconditions/postconditions](https://softwareengineering.stackexchange.com/q/345405)

- [Is this a violation of the Liskov Substitution Principle?](https://softwareengineering.stackexchange.com/q/170138)