0 XP · 0 scenes

Liskov Substitution

A subclass must work wherever its parent is expected
The problem

Penguin extends Bird, and Bird has fly().

The idea

Do not promise in the parent what a child cannot keep.

Reach for it when

A subclass overrides a method to throw or do nothing

1

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.

2

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.

✗ Before
migrate(birds)Birdfly()SparrowPenguinfly() ← raises error!fly()
  • extends (is a)
  • calls / uses
Bird promises fly(), but Penguin cannot keep that promise. migrate() crashes.
Reach for it when
  • 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
3

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.

✓ After
Birdeat()FlyingBirdfly()Penguinswim()Sparrowmigrate(birds)fly()
  • extends (is a)
  • calls / uses
Only birds that can really fly are FlyingBird. Penguin never reaches migrate().

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

  1. 1Look for overrides that throw, do nothing or narrow the input.
  2. 2Ask what the parent really promises to every child.
  3. 3Move the shaky promise into a narrower parent or interface.
  4. 4Make callers accept the narrower type.
4

Watch it run

Step through the calls. Tap an arrow to jump to it.

ClientmigrateSparrowPenguin1migrate([Sparrow(), Sparrow()])2fly()3'flies'4swim()
Step 1/4

migrate() accepts only FlyingBird objects, so every bird in the list keeps the promise.

migrate([Sparrow(), Sparrow()])
5

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

6

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?

Confidence