"L" - Liskov Substitution Principle. This principle says: if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.
Yes, I know, from the definition it's really unclear. Let's try to explain a little bit differently: functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
And yes, the second definition is also not such good for describing. Let me describe it a little bit simpler way. For example, you have class "Car", and you use this class in different places. This principle says, that each place, where you use the "Car" class, could be replaced (if needed) with any subclassed of the Car class.
So, if we have class "passenger car" which inherits from "Car" class or if we have "SUV" class, which also inherits from "Car" class and if we need to replace from "car" class to "SUV" or "passenger" class, after replacing from one class to any of subclasses, our system should work properly as before.
If you don't understand yet, let me show you some examples which should help you.
The first easy and common example is about shapes. We have a "Rectangle" class. From a mathematics point of view, "Square" is also "Rectangle". Knowing this, we can create a base "Rectangle" class and "Square" class, which extends from "Rectangle"
We created a simple "Rectangle" class, which has width, height, and methods. Now, let's create "Square" and extends from "Rectangle", because as we know, "Square" is a "Rectangle".
But, "Square" calculate area in a little bit different way, our height and width should be the same, so, let's rewrite a little bit our setWidth, setHeight logic:
Let's look at an example, of how can we use our classes, we will create 2 instances, check the area, after that change size and check the area again:
We can see that everything is ok and our code works as expected. No problems. But, let's look at Liskov Substitution Principle once more: If we change our base class for any of the subclasses, our system should work as before.
Let's do it and check it. We will change from the "Rectangle" class to the "Square" class. See the example below, I changed from
and that's all:
The code, before changing the size works the same, result of
the same. But, after
returns, a different result, not that was before (and it should return a different value). But now, we break the Liskov Substitution principle.
How can we solve it? The solution is to make inheritance, not from the "Rectangle" class, but prepare a more "correct" class for this. Just, for example, create some "Shape" class, that will calculate only area. Let's see:
So, now, we created a more common base class Shape and if we use this
somewhere, we can easily change from "Shape" to any of his subclasses, without breaking logic.
In our example, Rectangle and Square are different objects, that contain a little bit similar logic, but at the same time different logic. So, it will be more correct to divide them and not to use them as a "Parent-Child" class.
Let's take a look at another example, it's not mine, but it is good for describing and understanding this principle.
We want to create a "Bird" class. And we are thinking about which methods should be there. From the first point of view, we could think to add the method "fly" because all birds can fly.
And now, we want to create birds and say them fly:
Ok, after that, we realized that we have different kinds of birds and we want to create them: ducks, parrots, and swans:
And now, Liskov Substitution Principle says, that if we want to change from base class to subclass, the system should work, as it was before:
And yes, we changed arguments when we call "fly" function, instead of new Bird, now we call new Duck(), new Parrot(), new Swan(). Everything is still working and we correctly follow the Liskov Substitution Principle.
And now, we understand, that we want to add one more bird, a penguin. But, unfortunately, penguins can't fly. So, what we do, we will throw an error, if someone wants to call the "fly" method:
But now, we have a problem, our fly method doesn't expect error inside it and the "fly" function was created only for that birds, who can fly. Penguins can't fly, so we break Liskov Substitution Principle.
What is the solution? Instead of creating a base "Bird" class and using it as a flying bird, let's create a Flying bird class and all birds, who can fly, will extend only from Flying bird class and our "fly" function, will accept only Flying birds:
Notice, our Penguin class extends just from Bird class, not from FlyingBird and that's why now, we don't need the fly method, which throws an error.
Now, at any time, if we call somewhere new FlyingBird, and after it, we want to make a more specific bird, like Duck, Parrot, Swan we can easily change them instead of FlyingBird, and our code will work without problem.
In the next article, we will continue to learn SOLID and the next "L" letter.
See you there.