Liskov Substitution Principle
4 3 votes
Article Rating

Liskov Substitution Principle isn’t Complex. Just Give it a Try

What is Liskov Substitution Principle? How to apply it? What are its violations? Is it really hard to understand? Let’s know more about it.

Introduction

As we all know, software requirements always change, and we, as developers, need to make sure 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 created by Robert C. Martin (Uncle Bob). These principles help us create more flexible, maintainable, and understandable software. These principles are:

After introducing the Open-Closed Principle in the previous article, we will discuss the third principle, the Liskov Substitution Principle (LSP), which is the “L” in the SOLID acronym.

Definition

Let’s introduce the mathematical definition of the LSP and then jump into the details. Barbara Liskov introduced the mathematical definition 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 or Composition. 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 does that mean? Simply, it means 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?

How does LSP Extend the Open-Closed Principle?

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

As we will see, the LSP extends the Open-Closed principle, 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 must also conform to the Liskov Substitution Principle to avoid side effects.

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

Follows the Open-Closed principle, not the Liskov Substitution Principle
Follows OCP, not LSP

As you see, this example follows the OCP perfectly. 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 do 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 consider 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: some principles are more important than others. However, we should not ignore a principle because it is less important.

As we already knew, the LSP is about substituting a supertype and a subtype without impacting the existing client code. Keep this point in mind, and let’s jump into your solutions again:

  • Leaving it blank with no functionality: 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? It will break your existing code, won’t 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.

Liskov Substitution Principle Rules

Liskov Substitution Principle Rules
Photo by Tingey Injury Law Firm from Unsplash

Unfortunately, there is no easy way to enforce this principle in your code. However, to apply this principle correctly in your code, you must follow two types of rules: Signature and Behavior.

Although you can enforce the Signature rules using a compiled language like Java, you can’t enforce the Behavior rules. Instead, you must implement checks to enforce a specific behavior.

First, let’s introduce the Signature rules

  1. Countervariance 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.

If the client code expects a string-only argument in the SuperType, and you replace that a SuperType with SubType that accepts string or number arguments (wider), the client code will notice no differences.

  1. Covariance of return types: It converts 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.

The client code has already handled a string or number response coming from the SuperType. So if you replaced the SuperType with the SubType that returns only a string response, the client code wouldn’t break.

  1. Exceptions: The subtype method has to throw the same exceptions of the supertype or narrower. All compiled languages couldn’t enforce this rule. Some languages can enforce it, like Java, and others that can’t enforce it, like TypeScript.

Like the previous rule, as long as the client code 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.

Second, let’s introduce the Behavior rules

  1. Class Invariants (property rule): The subtype methods have to preserve or strengthen the supertype’s class invariants.

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, it wouldn’t be substitutable for the SuperType and would break the client code that depends on a specific behavior from the SuperType.

  1. History Constraint (property rule): The subtype methods shouldn’t allow a state change that the supertype doesn’t allow.

Here, if the SubType ignored the constraints imposed by the SuperType, this will break any client code that relies on these constraints. Therefore, SubType can’t be substitutable for SuperType.

  1. Preconditions (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.

In the previous example, any client code that provides the hour input imposed to the SuperType condition hour < 0 && hour > 12 will 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.

  1. Postconditions (method rule): The subtype method should preserve or strengthen the postconditions of the overridden supertype method.

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 replace it with a SubType returns a value with a maximum value of 30.

Confusion about Liskov Substitution Principle

Liskov Substitution Principle Confusion
Photo by bruce mars from Unsplash

At first glance, you might think 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 confused me a lot while learning this principle. I thought LSP could be applied only if I used inheritance.

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.

So, don’t tightly couple the LSP and inheritance concepts. Instead, keep in mind the LSP if you are forced to use inheritance. Look again at the example of the “How Does LSP Extend OCP?” section.

Liskov Substitution Principle Violation

Liskov Substitution Principle Violation
Photo by Jana Knorr from Unsplash

Let’s introduce the most common violations of LSP and try to redesign it to follow the LSP.

1) Type checking

If you’re checking the type of a variable inside a polymorphic code. Have a look at the below example:

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 is that you might add new types in the future and have to visit everywhere this check exists to add specific behaviors to support these new types. On top of that, this violates the Open-Closed Principle.

So, how can we solve this problem? One solution is using the Tell, Don’t Ask Principle or Encapsulation. It means don’t ask an instance for its type and then perform a specific action conditionally. Instead, encapsulate that logic in the type and tell it to perform an action. Let’s apply this to the previous example:

2) 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). This violates the LSP as well.

But how can we solve this problem? One common solution for this problem is using the Null Object design pattern. Have a look at this redesign:

3) Throwing not implemented exception

This is a common one because of the partial implementation of an interface or a base class. Look again at the example of the “How Does LSP Extend 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 is an interface or a base class.

However, you might argue that it is sometimes difficult to implement the interface fully, like in 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, or in other words, this is probably a violation of the Interface Segregation Principle.

Conclusion

In this article, we 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 subtypes are substitutable for supertypes.

We also knew the rules you must 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.

References

Think about it

If you liked this article please rate and share it to spread the word, really, that encourages me a lot to create more content like this.

If you found my content helpful, it is a good idea to subscribe to my newsletter. Make sure that I respect your inbox so I'll never spam you, and you can unsubscribe at any time!

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

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

4 3 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x
Scroll to Top