que-es-y-cuando-usar-polimorfismo-en-programacion

What is and how to use polymorphism

  • 10 min

Polymorphism is the last pillar of object-oriented programming we have yet to see. Polymorphism is the ability of an object to behave in different ways depending on the context in which it is used.

This 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 have to admit that polymorphism, as a word, sounds great. It’s one of those words that makes you feel smart just by saying it. It sounds so good 🤯. But, in reality it’s not as difficult as it seems at first.

Basically, polymorphism consists of the fact that,

An object of a child class can occupy a variable of the parent class, but preserving its own behavior.

Okay, that still sounds complicated. Better to see it with a code example. Here we go with another OOP classic: fruit examples!

Suppose we have a parent class Fruit, and two derived child classes Apple and Orange.

class Fruta
class Manzana  extends  Fruta
class Naranja  extends  Fruta
/// ... otras frutas
Copied!

What polymorphism says is that an Orange can occupy the place (a variable) of type Fruit. So,

// 👍 normal case, fruit object in fruit variable
Fruta miFruta = new Fruta();

// 🎭 POLYMORPHISM! I can put an orange in a variable of type fruit
Fruta miFruta = new Naranja();

// ❌ you CANNOT do this, because not all fruits are oranges
Naranja miNaranja = new Fruta();
Copied!

Or, going back to the mayor example, if you have a bus where Person can get on, Mayors can get on. And students. And plumbers. Because (hippie moment 🦄🌈) they are all people.

curso-poo-autobus

A bus carries people (with feelings)

How polymorphism works

Putting derived objects into variables of one of their parent types is only half the “trick”. To finish seeing 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 Fruit and Apple have a very simple method that writes their name to the console. “Fruit” and “Apple” respectively.

class Fruta
{
    DiTuNombre() { console.write("Fruta") }
}

class Manzana extends  Fruta
{
	// I override the method
    DiTuNombre() { console.write("Manzana") }
}
Copied!

What we want to know is what happens when we play at putting objects of one type into another type. We have the three valid cases.

// "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 mess, orange as fruit
Fruta miNaranjaFruta = new Naranja();
miNaranjaFruta.DiTuNombre();            // 🎭 POLYMORPHISM, prints "Naranja"
Copied!

Let’s see what happened:

  • 👍Fruit as fruit, and orange as orange, have no mystery. Each one calls its own method, and that’s it.
  • 🎭The “complicated” case is an Orange stored in Fruit. Here polymorphism comes into play, and the result is that it prints “orange”.

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, in other words, 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, on a bus seat, or on a park bench.

Did you understand? Follow me for more tips about people sitting on chairs 🪑.

When to use polymorphism

There are different situations where you can use polymorphism. Covering all cases would be impossible. But I’m going to mention two important ones that you will use frequently. They are also good examples of polymorphism usage.

For the example, imagine you have a geometric shape Shape, specialized into three child classes Rectangle, Triangle, and Circle.

class Figura {
	Dibujar() { /* ... */ }

	/* more things here... */
}

class Rectangulo extends Figura {
	Dibujar() { /* ... */ }
}

class Triangulo extends Figura {
	Dibujar() { /* .. */ }
}

class Circulo extends Figura {
	Dibujar() { /*... */ }
}
Copied!

All of them have their Draw() method overridden to draw themselves correctly on the screen, each with its own peculiarities.

Functions with parent class parameters

The first example: making a function that receives a Shape, and therefore can receive Rectangle, Triangle, and Circle.

function ProcesarFigura(Figura figura)
{
	figura.Dibujar();

	// do other things with shape
}
Copied!

Having the method overloaded, the function simply calls Draw(), and each object will draw itself correctly.

This way, the ProcessShape function doesn’t have to know details about how to draw the objects; each object knows that. It only knows that it wants to draw objects, not how to do it.

Furthermore, it allows us to reuse code. The ProcessShape function can work in the future, even if you add more types of Shapes.

Collections of parent classes

Another very common example is having a collection of elements where we want to store several more specific types. For example, you have a collection of Shape elements.

Figuras[] ListadoFiguras;
Copied!

Thanks to polymorphism, we can store objects of type Rectangle, Triangle, Circle inside.

Finally, when we want to draw them, we iterate through the entire collection and invoke the Draw() method.

ListadoFiguras.foreach(Dibujar);
Copied!

Each shape will be drawn correctly, calling the appropriate method.

Example of polymorphism

Let’s see what the syntax would be for implementing polymorphism between classes in different programming languages.

In C++, we define a base class Fruit with a virtual method SayYourName() that returns “I am a fruit”. Then we create a subclass called Orange that inherits from Fruit and overrides the SayYourName() method, returning “I am an orange”.

// Definition of the base class Fruit
class Fruit {
public:
    // Virtual method that shows generic fruit information
    virtual std::string SayYourName() {
        return "I am a fruit";
    }
};

// Definition of the Orange subclass that inherits from Fruit
class Orange : public Fruit {
public:
    // Overridden method that shows specific orange information
    std::string SayYourName() 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
}
Copied!

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.
Copied!

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

Copied!

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
Copied!

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.

pilares-oop

The four pillars of OOP

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).