Liskov Substitution
Penguin extends Bird, and Bird has fly().
Do not promise in the parent what a child cannot keep.
A subclass overrides a method to throw or do nothing
The picture from life
You hire a substitute teacher for a class. Whoever arrives must be able to teach the class: take attendance, explain, mark papers. A "substitute" who refuses to teach breaks the arrangement, even if he wears the same ID card.
The problem
What goes wrong when you do not use it.
Penguin extends Bird, and Bird has fly(). The penguin overrides fly() to throw an error. Now any code that loops over birds and calls fly() crashes the moment a penguin walks in.
- extends (is a)
- calls / uses
- A subclass overrides a method to throw or do nothing
- Callers need instanceof checks before using a child
- Overriding makes the result "surprising" for the parent's users
The idea
How the pattern fixes it.
Do not promise in the parent what a child cannot keep. Split the hierarchy by the real ability: only FlyingBird promises fly(). Every subclass can then be swapped in without surprise.
- extends (is a)
- calls / uses
Who is who
- Bird
- Only what every bird can do (eat).
- FlyingBird
- Adds fly(). Only creatures that really fly extend it.
- Penguin, Sparrow
- Each extends the right parent for its real ability.
Build it in steps
- 1Look for overrides that throw, do nothing or narrow the input.
- 2Ask what the parent really promises to every child.
- 3Move the shaky promise into a narrower parent or interface.
- 4Make callers accept the narrower type.
Watch it run
Step through the calls. Tap an arrow to jump to it.
migrate() accepts only FlyingBird objects, so every bird in the list keeps the promise.
migrate([Sparrow(), Sparrow()])The code
Python and Java. Try writing the classes yourself from the diagram first.
class Bird:
def eat(self):
return "eats"
class FlyingBird(Bird):
def fly(self):
return "flies"
class Sparrow(FlyingBird):
pass
class Penguin(Bird):
def swim(self):
return "swims"
def migrate(birds): # only birds that promise fly()
return [bird.fly() for bird in birds]
def demo():
return migrate([Sparrow(), Sparrow()]), Penguin().swim()What you gain
- Callers never need instanceof or special cases
- Subclasses can be swapped freely
What it costs
- More small types to name
- Real-world hierarchies rarely match a tidy tree
You have already met it in
- Java's UnsupportedOperationException on List.of() is a known LSP wobble
- Square extends Rectangle is the classic textbook violation
Often mixed up with
- Interface SegregationISP splits fat interfaces; LSP is about children keeping the parent's promises.
Make it stick
Model Rectangle and Square so that code written for Rectangle still works when handed a Square. What would you change in the design?