Polymorphism is the last pillar we have yet to see in object-oriented programming. Polymorphism is the ability for an object to behave in different ways depending on the context in which it is used.
Which is a very theoretical and nice definition, but not very practical. In other words,
Polymorphism is a very complex way of saying that a mayor is a person.
We must acknowledge that polymorphism, as a word, sounds very good. It’s one of those words that makes you feel smart just saying it. How good it sounds 🤯. But, in reality, it’s not as difficult as it seems at first.
Basically, polymorphism consists of:
An object of a child class of another can occupy a variable of the parent class while preserving its behavior.
Okay, that still sounds complicated. Let’s see it with a code example. Here we go with another classic of OOP: examples with fruits!
Suppose we have a parent class Fruta
, and two derived child classes Manzana
and Naranja
.
class Fruta
class Manzana extends Fruta
class Naranja extends Fruta
/// ... other fruits
What polymorphism says is that a Naranja
can occupy the place (a variable) of type Fruta
. So,
// 👍 normal case, fruit object in fruit variable
Fruta miFruta = new Fruta();
// 🎭 POLYMORPHISM! I can put an orange in a fruit-type variable
Fruta miFruta = new Naranja();
// ❌ you CANNOT do this, because not all fruits are oranges
Naranja miNaranja = new Fruta();
Or, going back to the mayor example, if you have a bus where Persona
can get on, mayors can get on. And students. And plumbers. Because (hippie moment 🦄🌈) they are all people.
How polymorphism works
Putting derived objects in variables of one of their parent classes is only half of the “grace.” To fully understand polymorphism, we need to talk about how an object behaves when it occupies a variable of another type that is not its own.
For this, we have to remember that child classes, in addition to inheriting properties and methods from their parents, can override some or all of them.
For example, suppose Fruta
and Manzana
have a very simple method that writes their name to the console. “Fruta” and “Manzana,” respectively.
class Fruta
{
DiTuNombre() { console.write("Fruta") }
}
class Manzana extends Fruta
{
// I override the method
DiTuNombre() { console.write("Manzana") }
}
What we want to know is what happens when we play at putting objects of one type into another type. We have three valid assumptions.
// normal fruit
Fruta miFruta = new Fruta();
miFruta.DiTuNombre(); // 👍 Easy case, prints "Fruta"
// orange "orange"
Naranja miNaranja = new Naranja();
miNaranja.DiTuNombre(); // 👍 Easy case, prints "Naranja"
// here’s the twist, orange as fruit
Fruta miNaranjaFruta = new Naranja();
miNaranjaFruta.DiTuNombre(); // 🎭 POLYMORPHISM, prints "Naranja"
Let’s see what happened:
- 👍Fruta as fruit, and naranja as orange, have no more mystery. Each calls the method that belongs to it, and that’s it.
- 🎭The “complicated” case is a
Naranja
stored inFruta
. Here comes polymorphism, and the result is that it prints “naranja.”
Which is actually not that complicated. When calling a method, the method of the object you have is called. The type of variable doesn’t matter; what matters is what you actually have stored in that variable.
Or, put another way, if you ask a mayor, a teacher, or a plumber about their profession, they will tell you their profession. It doesn’t matter if they are sitting in their office chair, in a bus seat, or on a park bench.
Is that understood? Follow me for more advice on people sitting in chairs 🪑.
When to use polymorphism
There are different situations where you can use polymorphism. Covering all cases would be impossible. But I will mention two important ones that you will frequently use. They are also good examples of polymorphism usage.
For the example, imagine you have a geometric shape Figura
, specialized in three child classes: Rectangulo
, Triangulo
, and Circulo
.
class Figura {
Dibujar() { /* ... */ }
/* more stuff here... */
}
class Rectangulo extends Figura {
Dibujar() { /* ... */ }
}
class Triangulo extends Figura {
Dibujar() { /* .. */ }
}
class Circulo extends Figura {
Dibujar() { /*... */ }
}
All of them have their Dibujar()
method overridden to draw themselves correctly on the screen, each with its peculiarities.
Functions with parent class parameter
The first example is making a function that receives a Figura
, and therefore can receive Rectangulo
, Triangulo
, and Circulo
.
function ProcesarFigura(Figura figura)
{
figura.Dibujar();
// do other things with figura
}
Having the method overridden, the function simply calls Dibujar()
, and each object will draw itself correctly.
In this way, the function ProcesarFigura
does not have to know the details of how to draw the objects; each object knows that. It only knows that it wants to draw objects, not how to do it.
Additionally, it allows us to reuse code. The function ProcesarFigura
can work in the future, even if you add more types of Figuras
.
Collections of parent classes
Another very common example is having a collection of elements where we want to store several more specific types. For instance, you have a collection of Figura
elements.
Figuras[] ListadoFiguras;
Thanks to polymorphism, we can store objects of type Rectangulo
, Triangulo
, Circulo
inside.
Finally, when we want to draw them, we iterate through the entire collection and invoke the Dibujar()
method.
ListadoFiguras.foreach(Dibujar);
Each figure will be drawn correctly, calling the appropriate method.
Example of polymorphism in different languages
Let’s see how the syntax for creating polymorphism between classes would look in different programming languages.
In C++, we define the base class Fruta
with a virtual method DiTuNombre()
that returns “I am a fruit.” Then we create a subclass called Naranja
that inherits from Fruta
and overrides the method DiTuNombre()
to return “I am an orange.”
// Definition of the base class Fruta
class Fruta {
public:
// Virtual method that shows generic fruit information
virtual std::string DiTuNombre() {
return "I am a fruit";
}
};
// Definition of the subclass Naranja that inherits from Fruta
class Naranja : public Fruta {
public:
// Overridden method that shows specific information about the orange
std::string DiTuNombre() override {
return "I am an orange";
}
};
int main() {
// Create instances of Fruta and Naranja
Fruta* fruta = new Fruta();
Fruta* naranja = new Naranja();
std::cout << fruta->DiTuNombre() << std::endl; // Output: I am a fruit
std::cout << naranja->DiTuNombre() << std::endl; // Output: I am an orange
}
The case of C# is very similar to the previous one. The base class Fruta
has a virtual method DiTuNombre()
. The Naranja
class inherits from Fruta
and overrides the method with its own DiTuNombre()
.
// Definition of the base class Fruta
public class Fruta
{
public virtual string DiTuNombre()
{
return $"I am a fruit";
}
}
// Definition of the subclass Naranja that inherits from Fruta
public class Naranja : Fruta
{
// Overridden method that shows specific information about the orange
public override string DiTuNombre()
{
return $"I am an orange";
}
}
// Create instances of Fruta and Naranja
Fruta fruta = new Fruta();
Fruta naranja = new Naranja();
Console.WriteLine(fruta.DiTuNombre()); // Output: This is a fruit called Manzana.
Console.WriteLine(naranja.DiTuNombre()); // Output: This is a fruit called Naranja. It is orange in color.
Now let’s see polymorphism in JavaScript
. Again, we create the base class Fruta
with its method DiTuNombre()
. The class Naranja
extends the Fruta
class and overrides the method.
// Definition of the base class Fruta
class Fruta {
// Virtual method that shows generic fruit information
DiTuNombre() {
return "I am a fruit";
}
}
// Definition of the subclass Naranja that inherits from Fruta
class Naranja extends Fruta {
// Overridden method that shows specific information about the orange
DiTuNombre() {
return "I am an orange";
}
}
// Create instances of Fruta and Naranja
const fruta = new Fruta();
const naranja = new Naranja();
// Call the DiTuNombre() method on both instances
console.log(fruta.DiTuNombre()); // Output: I am a fruit
console.log(naranja.DiTuNombre()); // Output: I am an orange
Finally, in Python we can also define our Fruta
class with a method di_tu_nombre()
. We then create a subclass Naranja
that inherits from Fruta
and overrides the method di_tu_nombre()
.
# Definition of the base class Fruta
class Fruta:
# Virtual method that shows generic fruit information
def di_tu_nombre(self):
return "I am a fruit"
# Definition of the subclass Naranja that inherits from Fruta
class Naranja(Fruta):
# Overridden method that shows specific information about the orange
def di_tu_nombre(self):
return "I am an orange"
# Create instances of Fruta and Naranja
fruta = Fruta()
naranja = Naranja()
# Call the di_tu_nombre() method on both instances
print(fruta.di_tu_nombre()) # Output: I am a fruit
print(naranja.di_tu_nombre()) # Output: I am an orange
As we can see, the concepts of polymorphism are more or less similar in all languages that include object-oriented programming, beyond syntax differences.
Where does polymorphism come from?
Just as we did with the other pillars, let’s look at the reasons that led to the invention of POLYMORPHISM. Remember that people were already doing data groupings. But if any part of the program could modify them, it would lead to some very interesting and unmanageable messes.
So they invented ENCAPSULATION. By being encapsulated, each class had to be independent, which forced a lot of duplicate code. To allow code reuse, INHERITANCE was introduced.
But with inheritance by itself, we couldn’t completely avoid having to repeat code. For example, in the case of Rectangulo
, Triangulo
, Circulo
, we would have to create all the functions that work with them, repeated for each type. To avoid this, POLYMORPHISM was introduced.
In summary:
- Encapsulation prevents uncontrolled modifications to objects.
- Inheritance allows us to avoid repeating class code.
- Polymorphism completes inheritance and allows me to avoid repeating the code that uses my classes (collections, functions).
Lastly, from the perspective of abstraction and modeling, POLYMORPHISM makes sense. Due to INHERITANCE, an Naranja has the same properties as Fruta because it is a specialization of Fruta (inheritance).