Liskov’s Substitution Principle — Let’s discuss something other than Squares and Rectangles.

Swati Kp
5 min readMar 21, 2021

--

Does it make any sense? 👩

Before I started writing this article, I talked to some of my colleagues (of course a meeting, which could have been an email) about their knowledge and thoughts on SOLID principles. Most of them told me the same old definitions, agreeing on the fact that Liskov’s Substitution Principle(LSP) is the one that is difficult to understand and even more difficult to implement. I don’t blame them, because the majority of articles on LSP on the internet are all about the Square-Rectangle problem. You are lucky if you haven’t heard about it before. I am not saying it's not a valid example but a little less practical. Please give it a read for the fun of it.

Okay, back to the problem — Just wanted to justify the title first. Let's start with an introduction to what this principle says and then we will continue with some examples that we can all relate to. If you haven’t read the previous article in the series, please read it here — Open Closed Principle.

The principle was initially introduced by Barbara Liskov in a 1987 conference keynote address titled Data abstraction and hierarchy. Barbara Liskov and Jeannette Wing described the principle succinctly in a 1994 paper as follows:

Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.

Don’t curse me for writing such a definition, this is what the paper said. But let's look at a definition that will help us to understand what it means for our software. The principle defines that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. In simple words, a client consuming the superclass/interface shouldn’t care about the underneath concrete implementation. In fact, the client should not even know about it. Basically, rather than worrying about the structure of the class, the principle says to enforce a specific behavior to it.

Let's try to understand the principle with an example. A warning before you start, the example is about some delicious cakes, so if you are hungry, proceed at your own risk.

Everyone loves cakes right. So let's open up a pretty little bakery that sells fresh cakes. This bakery wants to build software that automatically bakes cakes for them. They learned from the experience of the little restaurant in the previous article and decided to create a Base class — Cake so that they can bake all kinds of yummy cakes —eggless cakes, fruit cakes with different flavors, yummy right!

This cake class has 2 functions — ‘mixIngredients’ (which mixes ingredients list together) and ‘bake’(which bakes the ingredients to an actual cake.)

Which cake would you like to eat, Ummm, Chocolate Cake? Let's bake it.

The chocolate cake is almost ready, I promise. Let's just call the desired method from the client-side and eat the cake.

Everything looks perfect, right? I still have some space left in my stomach. Let's eat one more cake? This time, what about Fruit Cake? 😛

The little bakery decided to maintain the quality of the bakery products and hence made a rule that there should be at least 5 different varieties of fruits in order to bake a fruit cake.

But the poor naive client-side BakeFresh class doesn’t know that there is an extra-strong precondition to bake the fruit cake and the ingredients explode in the mixIngredients method with FruitNotSufficientException. 😣

What’s the problem here? Other than the fact that the fruit cake is not on my plate by now. There was an agreement between the Cake class and the client class that the ‘mixIngredients’ method will mix the cake ingredients, but it did not. Hence the contract is broken. This is the violation of LSP, as the subclass ‘FruitCake’ cannot be fitted everywhere in place of superclass — ‘Cake’.

Alright, the poor bakery understood the problem. But what's the solution? What if the client checks if it's a Fruit cake and then handles the FruitsNotSufficientException. And for other cakes, mixes the ingredients directly? Bad idea! Why? Because remember what LSP’s definition said at first? A client consuming the superclass/interface shouldn’t care about the underneath concrete implementation. Then why are we enforcing the client now to check the implementation before calling the methods? A clear violation of LSP.

What now? What about a shared interface ‘MixIngredients’ for Cakes that require simple mixing of ingredients and not put the ‘mixIngredients’ method in the ‘Cake’ class? How it will help? Firstly, no repetition of the ‘mixIngredients’ method in all the cakes that need simple mixing. Secondly, FruitCake class does not strengthen the preconditions for this method, as it does not override the method in the first place. And lastly, the client-side does not have to worry about the underlying implementation.

So the Cake class does not contain the ‘mixIngredients’ method now.

And the Ingredients interface will take care of the mixing.

And we create a yummy Chocolate cake:

And a yummy Fruit cake:

And now, the naive client cannot make the mistake of calling the ‘mixIngredients’ method on FruitCake class, as the method doesn’t exist. And when the client class uses ‘mixFruitCakeIngredients’, it already knows what it has gotten itself into i.e. the preconditions are already in the contract. Problem solved. The bakery will run happily ever after, or till the next article. 😜

Let's cover up some basic rules that LSP tells us to stick to.

  • No new exceptions: If there is an overridden method that does not throw any exception in the parent class but throws one in a derived class, that will result in the same issue that we just saw above. The code will explode when the client tries to call the method, being unaware of the new exception.
  • No extra-strong Pre-conditions: We saw a good enough example just now. The derived class should not strengthen the precondition for the methods overridden from the base class.
  • No Weak Post-conditions: Let’s assume you have a method in the base class to add new data to the DB that clears cache at the end. But you created a subclass and overridden the method that does not clear the cache. Now you have weakened the postcondition. The client-side without knowing the reason will get wrong cached values from the DB.

Liskov’s Substitution Principle like the previous principles is so simple yet offers a wide variety of benefits as shown — code reusability, easy maintainability, robustness, etc. We were using it all along without knowing there was any name for it. I hope you got to learn something new from the article and got some cravings for Cakes 😝. Let’s follow the same assignment from the previous blog and come up with a more practical example of the LSP and also think about all the places in your application, where you have violated the LSP.

Good news! We are done with the most difficult SOLID principle. Please wait for my next article on the Interface Segregation Principle, which in my opinion is the most up to the point and simple-to-understand principle.

Happy Learning!

--

--

Swati Kp

Tech Enthusiast. A poet by heart. Contributing my 2 cents for making this world a smart and better place to live.