Liskov Substitution is part of SOLID Design. SOLID?
SOLID are 5 software development principles or guidelines based on Object-Oriented design making it easier for you to make your projects scalable and maintainable.
Think of them as best practices.
Now What is Liskov Substitution
You see the L in SOLID stands for this principle.
It says
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. Barbara Liskov
Honestly, Too scientific.
In simple terms
Replacing an instance of a class with its child class should not produce any negative side effects or broken codebase.
Meaning
✔️ Can use the subclass of a parent class just the same as using the parent class without breaking anything.
✔️ Subclasses can modify/override parent class methods.
❌ Subclasses can modify the parent's method signature like arguments, return type, and exceptions.
❌ Subclasses can define a new function not present in the parent class.
❌ Parent class can be modified.
Why do this?
The Goal of this principle is to basically prevent our old codebase from breaking due to new code. This is also in line with the Single Responsibility and the Open Close Principle.
We will use a simple example for explanation.
A Simple Use Case
The following example violates the rule.
class Animal{
function eat(){
// common functionality
return "Eating Now" // return type string
}
function sleep(){
// common functionality
return "I am sleeping" // return type string
}
}
class Cat extends Animal{
function eat(){
// ... cat specific code
return "Meow, whatever human" // return type string
}
function sleep(){
// ... cat specific code
// voilating LSP: parnet sleep() does not return boolean
return true
}
}
class Dog extends Animal{
function eat(){
// ... dog specific code
return "Woof, It was tasty." // return type string
}
function sleep(){
// ... dog specific code
// voilating LSP: parent sleep() doesn't use Error Exception
throw Error('I just slept')
}
}
With the Liskov Substitution Principle, we would modify our code as follow
class Animal{
function eat(){
// common functionality
return "Eating Now" // return type string
}
function sleep(){
// common functionality
return "I am sleeping" // return type string
}
}
class Cat extends Animal{
function eat(){
// ... cat specific code
return "Meow, whatever human" // return type string
}
function sleep(){
// ... cat specific code
return "I am already sleeping" // return type string
}
}
class Dog extends Animal{
function eat(){
// ... dog specific code
return "Woof, It was actually tasty." // return type string
}
function sleep(){
// ... dog specific code
return "Zzzzzzzz" // return type string
}
}
With this approach, we can swap parent and child classes without breaking the code.
So Is it Helpful?
It is in most cases, but there are those cases where you might want to add some more that does not quite fit in like the Birds example below
class Bird{
function fly(){}
}
class Duck extends Bird{}
class Ostrich extends Bird{} // Duck can fly but ostrich cant:
So yeah, it really depends. If it's getting over-complicated/over-engineered or is not making sense(like bird example) then it's best to do your own thing.
Tip
It's easy to extend old code with new code. You just have to make a new class and extend it with parent/base class without the fear of breaking the already working code. We also get this benefit from Dependency Inversion principle.
So how do you see this? Do you think it's really useful? Be sure to tell me your opinion in the comments.
Top comments (3)
I might misunderstand the Liskov substitution principle but here's how I would read it:
In other words:
This is a condition to polymorphism and therefore a condition to the open-close principle and a way to avoid writing rigid softwares.
It also relates to the Interface Segregation principle as you don't want to bloat your classes with useless contracts that just prevent the interchangeabilility.
I admit the original definition is cryptic to me and not very useful.
As you pointed out, SOLID principles are meant to make your object oriented code scalable and maintainable. In my opinion, good practices have their place and should be used by default. However, there may be cases where a generally considered good practice just hinder you.
You have to know well what you are doing, but there might be code that doesn't need scalability, because is follows a rigid design that simply benefits from some characteristics of OOP. If you know (and I mean really know) the consequences of breaking the rules and are OK with it, then "the best practices" shouldn't be an obstacle.
Good Explanation, Miguel. I agree with that, thus left it open for the reader to decide.