The most popular description of this principle is simply
"A subclass must be replaceable with it's base class without breaking the program".
I have a couple of things to say about this. First, some general advice: correctness is not a popularity contest. Just because this description is “the most popular” does not make it right. It is, in fact, horribly wrong in at least three different ways.
Secondly, that description contains spelling mistakes. That alone should be a huge clue that it is not trustworthy.
I surely hope that you are wrong and this is in fact not “the most popular description”, because it is absolutely horrible, even ignoring the fact that apparently nobody ever noticed that “the most popular description” of the Liskov Substitution Principle isn’t even spelled properly. Thankfully, I haven’t seen this description before, so it doesn’t actually seem that popular.
So, with that out of the way, what is actually wrong with this description, other than the misspelling?
LSP is about types
The Liskov Substitution Principle is about types and subtyping, not about classes. Types and classes, subtyping and subclassing, subtyping and inheritance, are all different things. Do not confuse them.
There are plenty of programming languages that don’t have classes, for example C, Rust, Go, Pascal, Modula-2, and most importantly CLU (Barbara Liskov’s own programming language which she used to research what is now known as the Liskov Substitution Principle). There are plenty of programming languages where classes aren’t types, for example ECMAScript, Python, Ruby, Smalltalk, etc. There are plenty of programming languages where there are more types than just classes, for example Java, C++, C#, TypeScript. There are programming languages where subclassing and subtyping are distinct (e.g. Bard), and so on and so forth.
Frankly, how someone can think the LSP is about classes when Barbara Liskov’s own programming language doesn’t even have classes is beyond me. Liskov herself says the following in her OOPSLA87 keynote address Data Abstraction and Hierarchy:
We are using the words “subtype” and “supertype” here to emphasize that now we are talking about a semantic distinction. By contrast, “subclass” and “superclass” are simply linguistic concepts in programming languages that allow programs to be built in a particular way. They can be used to implement subtypes, but also, as mentioned above, in other ways.
LSP is about instances
The LSP is about replacing instances of a subtype for instances of a supertype.
It’s the wrong way around
The LSP says the exact opposite: I should be able to substitute instances of the subtype for instances of the supertype without changing the observable desirable properties of the program.
The LSP is descriptive, not prescriptive
Barbara Liskov formulated the property as “IFF S and T satisfy the following property, THEN S is a behavioral subtype of T”.
It was the OO community which later took this property and reversed it to say: “When S is a subtype of T, it must satisfy the following property”.
It makes sense to do this, because Barbara Liskov’s notion of behavioral subtyping has some nice properties, but in her own formulation, it is the property that induces the subtyping relationship, not the subtyping relationship that requires the property.
So, what is the LSP?
Well, Barbary Liskov never called it a “principle”, and she also didn’t name it after herself.
As mentioned in the preceding section, she called it a “subtyping property”, and there are two different variants of it, from Data Abstraction and Hierarchy (1987):
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.
And from A Behavioral Notion of Subtyping (1994):
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 <: T.
I don’t know what is “the most popular” definition, but these two are the correct definitions, which in my personal opinion is much more interesting than “the most popular”.
So, in simple terms: if I treat an instance of a subtype as if it were an instance of a supertype, I should not be able to tell the difference.
So if I have a base class Bird
, with the method eat()
, and a subclass FlyingBird
with the method fly()
And I have an array of flying birds FlyingBird[] birds
, if I wanted to call the method eat in each one of them with a loop, I should be able to use instances of Bird
rather than just FlyingBird
, and the program should not break.
This is again the wrong way around.
If you have a program that expects to work with Bird
s, I should be able to give it FlyingBird
s without breaking the program. In your example, this will work, since FlyingBird
s can also eat()
.
But then, what happens if I want to use the method fly? That method is no longer in Bird
, it's exclusive to FlyingBird
.
But you don’t want to use the method fly()
. You don’t even know that method exists. The program expects to work with Bird
s, it would never attempt to use fly()
because it does not know about that method. It only knows about eat()
.
According to the above description of LSP, it should be impossible for the program to break if I replace an instance of a subclass with its base class.
This is again the wrong way around. The LSP says that you should be able to replace an instance of the supertype with an instance of the subtype.
However, this statement seems super contradictory with inheritance, because then why would you want to have a subclass if it apparently cannot have unique methods?
It is perfectly sensible to have a subtype that does not have more operations than the supertype. It could, for example, be more efficient. For example, an integer set that uses a bit string representation is more efficient than a general set, but it supports the exact same set operations.
But of course a subtype can have additional operations, and you can use those operations through the API of the subtype. You just can’t use them through the API of the supertype, since they don't exist in that API.
So I think this could have 2 outcomes:
- I'm missing something about the principle and I don't understand it.
- That definition of the principle is pretty bad, and people should stop using it.
Which one it is?
All of the above.
I have also heard it being described as "Every subclass should still fullfill the contract of the base class", that definition sounds better as it doesn't implies that the subclass can't have new methods, but there's still people who swear by the "Should be to replace the class in code"
This is also correct. It is technically not the definition but rather the consequence of the definition.
So what is going on here?
What is going on here is that you have two correct definitions, the one you just quoted and the one in the picture you posted, and one wrong definition that says the exact opposite of the other two definitions, and it turns out that the wrong definition is, in fact, wrong.