Imagine you are writing the software for a games console and you want a single button that says
"Play". When it's pressed, a music file should play a song, a video file
should show a film, and a game should launch the level. You do not
want a giant tower of ifs asking "is this a song? is it a video? is it a game?" — you
just want to say thing.play() and trust each thing to do the right version of "play".
That is polymorphism — from the Greek for "many forms". It means the
same method call behaves differently depending on the actual type of the object it
is called on. You write one line, shape.area(), and a circle computes its area one way
while a rectangle computes it another — each object runs its own version of the method.
This is the pay-off of the object-oriented ideas you have already met. It rests directly on
Let's make it concrete with shapes. Every shape can report its area, but the
formula is different for each. We declare a base class Shape that promises an
area() method, then let each subclass override it — replace the
inherited method with its own version. Press Run:
Look closely at describe(). It lives in Shape and is written once,
yet it prints the correct area for both a circle and a rectangle. It calls this.area()
without knowing or caring which subclass this really is — the object supplies the right
version. That is polymorphism doing the work.
Here is where polymorphism truly earns its keep. We can put circles and rectangles into a
single array whose declared type is Shape[], then loop over it calling
area() on each. The variable shape is only ever declared as a
Shape — but at runtime each element runs its own version:
The loop body has no if testing the kind of shape — no
if (shape is Circle) … else if (shape is Rectangle) …. It works for circles,
rectangles and triangles alike, and — crucially — it would work for a Pentagon or a
Semicircle added years later, without changing a single line of this loop. New
subtype in, correct behaviour out. That is the real power: code that is open to extension.
Below, one message — area() — is sent to three different objects. Step through it: the
call is identical each time, but it is dispatched to whichever version
belongs to the object's real class. One interface, many implementations.
ifBefore polymorphism, the "many forms" job was done with a switch on a type tag — and it rots badly. Compare the two styles. The first must be edited every time a new shape appears; the second never needs touching:
The knowledge of "how to find my area" lives inside each shape, exactly where it belongs, instead of piling up in one ever-growing function elsewhere. Adding a shape is now a matter of writing a new self-contained class — not surgery on old code that already works.
Shapes and areas are one example, but the pattern is everywhere. Here every Animal can
speak(), and each species overrides it. A single function chorus() makes a
whole zoo speak — and it works for any animal you invent:
Two similar-sounding words trip up almost everyone. They are different, and only one of them powers polymorphism.
circle.area() and rectangle.area() behave differently, so
overriding is what powers polymorphism.
print(x: number) and
print(x: string)). The right one is chosen by the arguments you pass, not
by any object's type. This has nothing to do with subclasses or inheritance.
The other classic trap: the version that runs is chosen by the object's real (runtime)
type, not by the variable's declared type. In the snippet
below, s is declared as a Shape, but it is really a
Circle — so Circle's area() runs. The declared type only
decides which methods you're allowed to call; the real object decides which version
actually runs. Run it and see:
Poly means "many" and morph means "form" — the same word root
you meet in biology, where a species that comes in several forms is called polymorphic.
In computing, the "one thing, many forms" is a method call: the single call
shape.area() takes on the form of whichever concrete method the object provides. The
technical name for the machinery that picks the right one at runtime is dynamic
dispatch (sometimes "late binding") — the choice is made late, when the program
runs, once the object's real type is known.